mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/BE/US-35/stock-transfer' into 'development-before-sso'
(BE-58,,59): extend db schema & build stock transfer api See merge request mbugroup/lti-api!19
This commit is contained in:
@@ -10,10 +10,10 @@ import (
|
||||
// === DTO Structs ===
|
||||
|
||||
type ProductWarehouseBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProductId uint `json:"product_id"`
|
||||
WarehouseId uint `json:"warehouse_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Id uint `json:"id"`
|
||||
ProductId uint `json:"product_id"`
|
||||
WarehouseId uint `json:"warehouse_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
type ProductWarehouseListDTO struct {
|
||||
@@ -31,9 +31,10 @@ type ProductWarehouseDetailDTO struct {
|
||||
|
||||
// Nested DTOs for relations
|
||||
type ProductBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Sku string `json:"sku"`
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Sku string `json:"sku"`
|
||||
Flags []string `json:"flags"`
|
||||
}
|
||||
|
||||
type WarehouseBaseDTO struct {
|
||||
@@ -68,6 +69,12 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
|
||||
if e.Product.Sku != nil {
|
||||
product.Sku = *e.Product.Sku
|
||||
}
|
||||
// Map flags from Product relation
|
||||
if len(e.Product.Flags) > 0 {
|
||||
for _, f := range e.Product.Flags {
|
||||
product.Flags = append(product.Flags, f.Name)
|
||||
}
|
||||
}
|
||||
dto.Product = &product
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func NewProductWarehouseService(repo repository.ProductWarehouseRepository, vali
|
||||
}
|
||||
|
||||
func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Product").Preload("Warehouse").Preload("CreatedUser")
|
||||
return db.Preload("Product.Flags").Preload("Product").Preload("Warehouse").Preload("CreatedUser")
|
||||
}
|
||||
|
||||
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
|
||||
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
|
||||
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
|
||||
// MODULE IMPORTS
|
||||
)
|
||||
|
||||
@@ -19,6 +20,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
|
||||
productWarehouses.ProductWarehouseModule{},
|
||||
|
||||
adjustments.AdjustmentModule{},
|
||||
transfers.TransferModule{},
|
||||
// MODULE REGISTRY
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type TransferController struct {
|
||||
TransferService service.TransferService
|
||||
}
|
||||
|
||||
func NewTransferController(transferService service.TransferService) *TransferController {
|
||||
return &TransferController{
|
||||
TransferService: transferService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *TransferController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.TransferService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.TransferListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all transfers successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToTransferListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *TransferController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.TransferService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get transfer successfully",
|
||||
Data: dto.ToTransferListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
||||
data := c.FormValue("data")
|
||||
|
||||
var req validation.TransferRequest
|
||||
if err := json.Unmarshal([]byte(data), &req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
// ambil file
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
|
||||
}
|
||||
_ = form.File["documents"]
|
||||
// todo: tunggu ada aws baru proses
|
||||
|
||||
result, err := u.TransferService.CreateOne(c, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create transfer successfully",
|
||||
Data: dto.ToTransferListDTO(*result),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type TransferBaseDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
TransferReason string `json:"transfer_reason"`
|
||||
TransferDate string `json:"transfer_date"`
|
||||
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
|
||||
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
// Only id and name for warehouse simple view
|
||||
type WarehouseSimpleDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AreaDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type LocationDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type WarehouseDetailDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Location *LocationDTO `json:"location"`
|
||||
Area *AreaDTO `json:"area"`
|
||||
}
|
||||
|
||||
type TransferListDTO struct {
|
||||
TransferBaseDTO
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Details []TransferDetailItemDTO `json:"details"`
|
||||
Deliveries []TransferDeliveryDTO `json:"deliveries"`
|
||||
}
|
||||
|
||||
type TransferDetailDTO struct {
|
||||
TransferListDTO
|
||||
Details []TransferDetailItemDTO `json:"details"`
|
||||
Deliveries []TransferDeliveryDTO `json:"deliveries"`
|
||||
}
|
||||
|
||||
// Detail produk
|
||||
type TransferDetailItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
ProductId uint64 `json:"product_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
BeforeQuantity float64 `json:"before_quantity"`
|
||||
AfterQuantity float64 `json:"after_quantity"`
|
||||
}
|
||||
|
||||
// Delivery ekspedisi
|
||||
type TransferDeliveryDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
SupplierId uint64 `json:"supplier_id"`
|
||||
VehiclePlate string `json:"vehicle_plate"`
|
||||
DriverName string `json:"driver_name"`
|
||||
DocumentNumber string `json:"document_number"`
|
||||
DocumentPath string `json:"document_path"`
|
||||
ShippingCostItem float64 `json:"shipping_cost_item"`
|
||||
ShippingCostTotal float64 `json:"shipping_cost_total"`
|
||||
Items []TransferDeliveryItemDTO `json:"items"`
|
||||
}
|
||||
|
||||
type TransferDeliveryItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
StockTransferDetailId uint64 `json:"stock_transfer_detail_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
|
||||
|
||||
var sourceWarehouse *WarehouseDetailDTO
|
||||
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
||||
}
|
||||
var destinationWarehouse *WarehouseDetailDTO
|
||||
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
||||
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
|
||||
}
|
||||
return TransferBaseDTO{
|
||||
Id: e.Id,
|
||||
TransferReason: e.Reason,
|
||||
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
||||
SourceWarehouse: sourceWarehouse,
|
||||
DestinationWarehouse: destinationWarehouse,
|
||||
}
|
||||
}
|
||||
|
||||
func toAreaDTO(a *entity.Area) *AreaDTO {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return &AreaDTO{
|
||||
Id: a.Id,
|
||||
Name: a.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func toLocationDTO(l *entity.Location) *LocationDTO {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &LocationDTO{
|
||||
Id: l.Id,
|
||||
Name: l.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
|
||||
if w == nil {
|
||||
return nil
|
||||
}
|
||||
return &WarehouseDetailDTO{
|
||||
Id: w.Id,
|
||||
Name: w.Name,
|
||||
Location: toLocationDTO(w.Location),
|
||||
Area: toAreaDTO(&w.Area), // Ambil area langsung dari warehouse (area_id)
|
||||
}
|
||||
}
|
||||
|
||||
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||
var createdUser *userDTO.UserBaseDTO
|
||||
if e.CreatedUser != nil {
|
||||
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
// Map details
|
||||
var details []TransferDetailItemDTO
|
||||
for _, d := range e.Details {
|
||||
details = append(details, TransferDetailItemDTO{
|
||||
Id: d.Id,
|
||||
ProductId: d.ProductId,
|
||||
Quantity: d.Quantity,
|
||||
})
|
||||
}
|
||||
// Map deliveries
|
||||
var deliveries []TransferDeliveryDTO
|
||||
for _, del := range e.Deliveries {
|
||||
// Map delivery items
|
||||
var items []TransferDeliveryItemDTO
|
||||
for _, item := range del.Items {
|
||||
items = append(items, TransferDeliveryItemDTO{
|
||||
Id: item.Id,
|
||||
StockTransferDetailId: item.StockTransferDetailId,
|
||||
Quantity: item.Quantity,
|
||||
})
|
||||
}
|
||||
deliveries = append(deliveries, TransferDeliveryDTO{
|
||||
Id: del.Id,
|
||||
SupplierId: del.SupplierId,
|
||||
VehiclePlate: del.VehiclePlate,
|
||||
DriverName: del.DriverName,
|
||||
DocumentNumber: del.DocumentNumber,
|
||||
DocumentPath: del.DocumentPath,
|
||||
ShippingCostItem: del.ShippingCostItem,
|
||||
ShippingCostTotal: del.ShippingCostTotal,
|
||||
Items: items,
|
||||
})
|
||||
}
|
||||
return TransferListDTO{
|
||||
TransferBaseDTO: ToTransferBaseDTO(e),
|
||||
CreatedUser: createdUser,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
Details: details,
|
||||
Deliveries: deliveries,
|
||||
}
|
||||
}
|
||||
|
||||
func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
|
||||
result := make([]TransferListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToTransferListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
||||
// Map details
|
||||
var details []TransferDetailItemDTO
|
||||
for _, d := range e.Details {
|
||||
details = append(details, TransferDetailItemDTO{
|
||||
Id: d.Id,
|
||||
ProductId: d.ProductId,
|
||||
Quantity: d.Quantity,
|
||||
})
|
||||
}
|
||||
// Map deliveries
|
||||
var deliveries []TransferDeliveryDTO
|
||||
for _, del := range e.Deliveries {
|
||||
deliveries = append(deliveries, TransferDeliveryDTO{
|
||||
Id: del.Id,
|
||||
SupplierId: del.SupplierId,
|
||||
VehiclePlate: del.VehiclePlate,
|
||||
DriverName: del.DriverName,
|
||||
DocumentNumber: del.DocumentNumber,
|
||||
DocumentPath: del.DocumentPath,
|
||||
ShippingCostItem: del.ShippingCostItem,
|
||||
ShippingCostTotal: del.ShippingCostTotal,
|
||||
})
|
||||
}
|
||||
return TransferDetailDTO{
|
||||
TransferListDTO: ToTransferListDTO(e),
|
||||
Details: details,
|
||||
Deliveries: deliveries,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package transfers
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
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"
|
||||
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type TransferModule struct{}
|
||||
|
||||
func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
stockTransferRepo := rStockTransfer.NewStockTransferRepository(db)
|
||||
stockTransferDetailRepo := rStockTransfer.NewStockTransferDetailRepository(db)
|
||||
stockTransferDeliveryRepo := rStockTransfer.NewStockTransferDeliveryRepository(db)
|
||||
StockTransferDeliveryItemRepo := rStockTransfer.NewStockTransferDeliveryItemRepository(db)
|
||||
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
||||
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
TransferRoutes(router, userService, transferService)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type StockTransferRepository interface {
|
||||
repository.BaseRepository[entity.StockTransfer]
|
||||
// get sequence for movement number
|
||||
GetNextMovementNumber(ctx context.Context) (int64, error)
|
||||
}
|
||||
|
||||
type StockTransferRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.StockTransfer]
|
||||
}
|
||||
|
||||
func NewStockTransferRepository(db *gorm.DB) StockTransferRepository {
|
||||
return &StockTransferRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransfer](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *StockTransferRepositoryImpl) GetNextMovementNumber(ctx context.Context) (int64, error) {
|
||||
var seq int64
|
||||
err := r.DB().WithContext(ctx).Raw("SELECT nextval('stock_transfer_seq')").Scan(&seq).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return seq, nil
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type StockTransferDeliveryRepository interface {
|
||||
repository.BaseRepository[entity.StockTransferDelivery]
|
||||
// Tambahkan custom method jika perlu
|
||||
}
|
||||
|
||||
type StockTransferDeliveryRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.StockTransferDelivery]
|
||||
}
|
||||
|
||||
func NewStockTransferDeliveryRepository(db *gorm.DB) StockTransferDeliveryRepository {
|
||||
return &StockTransferDeliveryRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDelivery](db),
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type StockTransferDeliveryItemRepository interface {
|
||||
repository.BaseRepository[entity.StockTransferDeliveryItem]
|
||||
// Tambahkan custom method jika perlu
|
||||
}
|
||||
|
||||
type StockTransferDeliveryItemRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.StockTransferDeliveryItem]
|
||||
}
|
||||
|
||||
func NewStockTransferDeliveryItemRepository(db *gorm.DB) StockTransferDeliveryItemRepository {
|
||||
return &StockTransferDeliveryItemRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDeliveryItem](db),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Find all details by StockTransferId
|
||||
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type StockTransferDetailRepository interface {
|
||||
repository.BaseRepository[entity.StockTransferDetail]
|
||||
FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error
|
||||
}
|
||||
|
||||
type StockTransferDetailRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.StockTransferDetail]
|
||||
}
|
||||
|
||||
func NewStockTransferDetailRepository(db *gorm.DB) StockTransferDetailRepository {
|
||||
return &StockTransferDetailRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDetail](db),
|
||||
}
|
||||
}
|
||||
func (r *StockTransferDetailRepositoryImpl) FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error {
|
||||
return r.DB().WithContext(ctx).Where("stock_transfer_id = ?", transferId).Find(out).Error
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package transfers
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/controllers"
|
||||
transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferService) {
|
||||
ctrl := controller.NewTransferController(s)
|
||||
|
||||
route := v1.Group("/transfers")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
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/stock-logs/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TransferService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error)
|
||||
}
|
||||
|
||||
type transferService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
StockTransferRepo rStockTransfer.StockTransferRepository
|
||||
StockTransferDetailRepo rStockTransfer.StockTransferDetailRepository
|
||||
StockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository
|
||||
StockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository
|
||||
StockLogsRepository rStockLogs.StockLogRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
SupplierRepo rSupplier.SupplierRepository
|
||||
}
|
||||
|
||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService {
|
||||
return &transferService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
StockTransferRepo: stockTransferRepo,
|
||||
StockTransferDetailRepo: stockTransferDetailRepo,
|
||||
StockTransferDeliveryRepo: stockTransferDeliveryRepo,
|
||||
StockTransferDeliveryItemRepo: stockTransferDeliveryItemRepo,
|
||||
StockLogsRepository: stockLogsRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
SupplierRepo: supplierRepo,
|
||||
}
|
||||
}
|
||||
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("FromWarehouse").
|
||||
Preload("FromWarehouse.Location").
|
||||
Preload("FromWarehouse.Area").
|
||||
Preload("ToWarehouse").
|
||||
Preload("ToWarehouse.Location").
|
||||
Preload("ToWarehouse.Area").
|
||||
Preload("Details").
|
||||
Preload("Deliveries.Items")
|
||||
}
|
||||
|
||||
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
s.Log.Infof("Retrieved %d transfers", len(transfers))
|
||||
|
||||
return transfers, total, nil
|
||||
|
||||
}
|
||||
|
||||
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
||||
var transfer entity.StockTransfer
|
||||
|
||||
// gunakan repo secara langsung
|
||||
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return s.withRelations(db)
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get transfer by ID: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
||||
}
|
||||
|
||||
s.Log.Infof("Retrieved transfer: %+v", transfer)
|
||||
|
||||
return transferPtr, nil
|
||||
}
|
||||
|
||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||
|
||||
// Validasi stok di gudang asal harus exist dan mencukupi
|
||||
for _, product := range req.Products {
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
|
||||
}
|
||||
if sourcePW.Quantity < product.ProductQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
||||
}
|
||||
}
|
||||
|
||||
// validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid
|
||||
deliveryQtyMap := make(map[uint]float64)
|
||||
for _, delivery := range req.Deliveries {
|
||||
for _, prod := range delivery.Products {
|
||||
deliveryQtyMap[prod.ProductID] += prod.ProductQty
|
||||
}
|
||||
}
|
||||
|
||||
// Cek: qty delivery tidak boleh melebihi qty di root
|
||||
for _, product := range req.Products {
|
||||
if deliveryQtyMap[product.ProductID] > product.ProductQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Total qty delivery untuk produk %d (%v) melebihi qty transfer (%v)", product.ProductID, deliveryQtyMap[product.ProductID], product.ProductQty))
|
||||
}
|
||||
}
|
||||
|
||||
// cek suplier id caegory BOP cek by id
|
||||
for _, delivery := range req.Deliveries {
|
||||
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
||||
}
|
||||
if supplier.Category != "BOP" {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
|
||||
}
|
||||
}
|
||||
|
||||
// Generate movement number
|
||||
// Format: PND-MBU-00001
|
||||
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get next movement number: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
||||
}
|
||||
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
|
||||
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
||||
|
||||
entityTransfer := &entity.StockTransfer{
|
||||
FromWarehouseId: uint64(req.SourceWarehouseID),
|
||||
ToWarehouseId: uint64(req.DestinationWarehouseID),
|
||||
Reason: req.TransferReason,
|
||||
TransferDate: transferDate,
|
||||
MovementNumber: movementNumber,
|
||||
CreatedBy: 1, //todo: get from token
|
||||
}
|
||||
|
||||
// Save the transfer entity to the database
|
||||
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
// Insert header
|
||||
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
|
||||
|
||||
// insert ke details
|
||||
var details []*entity.StockTransferDetail
|
||||
for _, product := range req.Products {
|
||||
details = append(details, &entity.StockTransferDetail{
|
||||
StockTransferId: entityTransfer.Id,
|
||||
ProductId: uint64(product.ProductID),
|
||||
Quantity: product.ProductQty,
|
||||
})
|
||||
}
|
||||
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer details: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
|
||||
|
||||
// Tambahkan proses insert delivery
|
||||
var deliveries []*entity.StockTransferDelivery
|
||||
for _, delivery := range req.Deliveries {
|
||||
deliveries = append(deliveries, &entity.StockTransferDelivery{
|
||||
StockTransferId: entityTransfer.Id,
|
||||
SupplierId: uint64(delivery.SupplierID),
|
||||
VehiclePlate: delivery.VehiclePlate,
|
||||
DriverName: delivery.DriverName,
|
||||
DocumentPath: "dummy duls", // todo: tunggu ada aws baru proses
|
||||
ShippingCostItem: delivery.DeliveryCostPerItem,
|
||||
ShippingCostTotal: delivery.DeliveryCost,
|
||||
})
|
||||
}
|
||||
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
|
||||
return err
|
||||
}
|
||||
// tambahkan insert ke delivery items sebagai pivot
|
||||
detailMap := make(map[uint64]uint64)
|
||||
for _, d := range details {
|
||||
detailMap[d.ProductId] = d.Id
|
||||
}
|
||||
|
||||
var deliveryItems []*entity.StockTransferDeliveryItem
|
||||
|
||||
for i, delivery := range deliveries {
|
||||
item := req.Deliveries[i]
|
||||
for _, prod := range item.Products {
|
||||
detailID, ok := detailMap[uint64(prod.ProductID)]
|
||||
if !ok {
|
||||
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
||||
}
|
||||
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
||||
StockTransferDeliveryId: delivery.Id,
|
||||
StockTransferDetailId: detailID,
|
||||
Quantity: prod.ProductQty,
|
||||
})
|
||||
}
|
||||
}
|
||||
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer delivery items: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
|
||||
|
||||
// Proses pengurangan stok di gudang asal dan penambahan stok di gudang tujuan
|
||||
for _, product := range req.Products {
|
||||
// Kurangi stok di gudang asal
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
|
||||
}
|
||||
if sourcePW.Quantity < product.ProductQty {
|
||||
s.Log.Errorf("Insufficient stock in source warehouse for product ID: %+v", product.ProductID)
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock in source warehouse for product ID: %d", product.ProductID))
|
||||
}
|
||||
sourcePW.Quantity -= product.ProductQty
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), sourcePW.Id, sourcePW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to update source product warehouse: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
||||
|
||||
// Tambah stok di gudang tujuan
|
||||
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||
)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to get destination product warehouse: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
|
||||
}
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Jika belum ada record untuk produk di gudang tujuan, buat baru
|
||||
destPW = &entity.ProductWarehouse{
|
||||
ProductId: uint(product.ProductID),
|
||||
WarehouseId: uint(req.DestinationWarehouseID),
|
||||
Quantity: 0,
|
||||
CreatedBy: 1, // TODO: should Get from auth middleware
|
||||
}
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create destination product warehouse")
|
||||
}
|
||||
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
|
||||
}
|
||||
// Update stok di gudang tujuan
|
||||
destPW.Quantity += product.ProductQty
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
|
||||
}
|
||||
|
||||
// Ambil data lengkap hasil create dengan GetOne (agar preload relasi sama dengan GetOne)
|
||||
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type TransferProduct struct {
|
||||
ProductID uint `json:"product_id" validate:"required"`
|
||||
ProductQty float64 `json:"product_qty" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type TransferDeliveryProduct struct {
|
||||
ProductID uint `json:"product_id" validate:"required"`
|
||||
ProductQty float64 `json:"product_qty" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type TransferDelivery struct {
|
||||
DeliveryCost float64 `json:"delivery_cost" validate:"required"`
|
||||
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
|
||||
DocumentIndex int `json:"document_index" validate:"min=0"`
|
||||
DriverName string `json:"driver_name" validate:"required"`
|
||||
VehiclePlate string `json:"vehicle_plate" validate:"required"`
|
||||
SupplierID uint `json:"supplier_id" validate:"required"`
|
||||
Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type TransferRequest struct {
|
||||
TransferReason string `json:"transfer_reason" validate:"required"`
|
||||
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
|
||||
SourceWarehouseID uint `json:"source_warehouse_id" validate:"required"`
|
||||
DestinationWarehouseID uint `json:"destination_warehouse_id" validate:"required"`
|
||||
Products []TransferProduct `json:"products" validate:"required,dive"`
|
||||
Deliveries []TransferDelivery `json:"deliveries" validate:"required,dive"`
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type SupplierRepository interface {
|
||||
repository.BaseRepository[entity.Supplier]
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
|
||||
}
|
||||
|
||||
type SupplierRepositoryImpl struct {
|
||||
|
||||
Reference in New Issue
Block a user