mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat(BE-58,59,60,61): implement stock transfer API, validation, audit log, and schema update
- Build stock transfer API with nested details, deliveries, and items - Extend DB schema for stock transfers - Implement validation for transfer request and stock - Prepare/implement transfer audit log structure - Preload all relations for complete response - Update DTOs for nested response - Remove redundant root fields, use relation objects
This commit is contained in:
@@ -316,7 +316,7 @@ CREATE TABLE stock_logs (
|
|||||||
before_quantity NUMERIC(15, 3) NOT NULL,
|
before_quantity NUMERIC(15, 3) NOT NULL,
|
||||||
after_quantity NUMERIC(15, 3) NOT NULL,
|
after_quantity NUMERIC(15, 3) NOT NULL,
|
||||||
log_type VARCHAR(50) NOT NULL,
|
log_type VARCHAR(50) NOT NULL,
|
||||||
log_id BIGINT ,
|
log_id BIGINT,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ type StockTransfer struct {
|
|||||||
ToWarehouse *Warehouse `gorm:"foreignKey:ToWarehouseId"`
|
ToWarehouse *Warehouse `gorm:"foreignKey:ToWarehouseId"`
|
||||||
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
||||||
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,48 +4,146 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type TransferBaseDTO struct {
|
type TransferBaseDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
TransferReason string `json:"transfer_reason"`
|
TransferReason string `json:"transfer_reason"`
|
||||||
TransferDate string `json:"transfer_date"`
|
TransferDate string `json:"transfer_date"`
|
||||||
SourceWarehouseId uint64 `json:"source_warehouse_id"`
|
SourceWarehouse *WarehouseSimpleDTO `json:"source_warehouse,omitempty"`
|
||||||
DestinationWarehouseId uint64 `json:"destination_warehouse_id"`
|
DestinationWarehouse *WarehouseSimpleDTO `json:"destination_warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only id and name for warehouse simple view
|
||||||
|
type WarehouseSimpleDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferListDTO struct {
|
type TransferListDTO struct {
|
||||||
TransferBaseDTO
|
TransferBaseDTO
|
||||||
CreatedBy uint64 `json:"created_by"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Details []TransferDetailItemDTO `json:"details"`
|
||||||
|
Deliveries []TransferDeliveryDTO `json:"deliveries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferDetailDTO struct {
|
type TransferDetailDTO struct {
|
||||||
TransferListDTO
|
TransferListDTO
|
||||||
// Tambahkan detail produk, deliveries, dsb jika perlu
|
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"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
Items []TransferDeliveryItemDTO `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferDeliveryItemDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
StockTransferDetailId uint64 `json:"stock_transfer_detail_id"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
|
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
|
||||||
|
var sourceWarehouse *WarehouseSimpleDTO
|
||||||
|
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||||
|
sourceWarehouse = &WarehouseSimpleDTO{
|
||||||
|
Id: e.FromWarehouse.Id,
|
||||||
|
Name: e.FromWarehouse.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var destinationWarehouse *WarehouseSimpleDTO
|
||||||
|
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
||||||
|
destinationWarehouse = &WarehouseSimpleDTO{
|
||||||
|
Id: e.ToWarehouse.Id,
|
||||||
|
Name: e.ToWarehouse.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
return TransferBaseDTO{
|
return TransferBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
TransferReason: e.Reason, // atau field lain sesuai entity
|
TransferReason: e.Reason,
|
||||||
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
||||||
SourceWarehouseId: e.FromWarehouseId,
|
SourceWarehouse: sourceWarehouse,
|
||||||
DestinationWarehouseId: e.ToWarehouseId,
|
DestinationWarehouse: destinationWarehouse,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
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,
|
||||||
|
BeforeQuantity: d.BeforeQuantity,
|
||||||
|
AfterQuantity: d.AfterQuantity,
|
||||||
|
Note: d.Note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 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,
|
||||||
|
Note: del.Note,
|
||||||
|
Items: items,
|
||||||
|
})
|
||||||
|
}
|
||||||
return TransferListDTO{
|
return TransferListDTO{
|
||||||
TransferBaseDTO: ToTransferBaseDTO(e),
|
TransferBaseDTO: ToTransferBaseDTO(e),
|
||||||
CreatedBy: e.CreatedBy,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
Details: details,
|
||||||
|
Deliveries: deliveries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +156,36 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
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,
|
||||||
|
BeforeQuantity: d.BeforeQuantity,
|
||||||
|
AfterQuantity: d.AfterQuantity,
|
||||||
|
Note: d.Note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 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,
|
||||||
|
Note: del.Note,
|
||||||
|
})
|
||||||
|
}
|
||||||
return TransferDetailDTO{
|
return TransferDetailDTO{
|
||||||
TransferListDTO: ToTransferListDTO(e),
|
TransferListDTO: ToTransferListDTO(e),
|
||||||
|
Details: details,
|
||||||
|
Deliveries: deliveries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -1,6 +1,10 @@
|
|||||||
|
// Find all details by StockTransferId
|
||||||
|
|
||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -8,7 +12,7 @@ import (
|
|||||||
|
|
||||||
type StockTransferDetailRepository interface {
|
type StockTransferDetailRepository interface {
|
||||||
repository.BaseRepository[entity.StockTransferDetail]
|
repository.BaseRepository[entity.StockTransferDetail]
|
||||||
// Tambahkan custom method jika perlu
|
FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type StockTransferDetailRepositoryImpl struct {
|
type StockTransferDetailRepositoryImpl struct {
|
||||||
@@ -20,3 +24,6 @@ func NewStockTransferDetailRepository(db *gorm.DB) StockTransferDetailRepository
|
|||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDetail](db),
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
@@ -46,9 +47,13 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
|||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db.Preload("CreatedUser")
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("FromWarehouse").
|
||||||
|
Preload("ToWarehouse").
|
||||||
|
Preload("Details").
|
||||||
|
Preload("Deliveries.Items")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
|
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
|
||||||
@@ -56,26 +61,37 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
// transfers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
// db = s.withRelations(db)
|
db = db.Preload("CreatedUser").Preload("FromWarehouse").Preload("ToWarehouse").Preload("Details").Preload("Deliveries.Items")
|
||||||
// if params.Search != "" {
|
if params.Search != "" {
|
||||||
// return db.Where("name LIKE ?", "%"+params.Search+"%")
|
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
||||||
// }
|
}
|
||||||
// return db.Order("created_at DESC").Order("updated_at DESC")
|
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
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// s.Log.Errorf("Failed to get transfers: %+v", err)
|
|
||||||
// return nil, 0, err
|
|
||||||
// }
|
|
||||||
return nil, 0, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
||||||
|
var transfer entity.StockTransfer
|
||||||
return nil, nil
|
db := s.StockTransferRepo.DB().WithContext(c.Context())
|
||||||
|
db = db.Preload("CreatedUser").Preload("FromWarehouse").Preload("ToWarehouse").Preload("Details").Preload("Deliveries.Items")
|
||||||
|
if err := db.First(&transfer, id).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
||||||
|
}
|
||||||
|
return &transfer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||||
@@ -160,16 +176,31 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
|
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// tambahkan insert ke delivery items sebagai fivot
|
// tambahkan insert ke delivery items sebagai fivot
|
||||||
|
|
||||||
|
detailMap := make(map[uint64]uint64)
|
||||||
|
for _, d := range details {
|
||||||
|
detailMap[d.ProductId] = d.Id
|
||||||
|
}
|
||||||
|
|
||||||
var deliveryItems []*entity.StockTransferDeliveryItem
|
var deliveryItems []*entity.StockTransferDeliveryItem
|
||||||
for i, delivery := range req.Deliveries {
|
|
||||||
for _, item := range delivery.Products {
|
for _, delivery := range deliveries {
|
||||||
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
for _, item := range req.Deliveries {
|
||||||
StockTransferDeliveryId: deliveries[i].Id,
|
if item.Document == delivery.DocumentPath {
|
||||||
StockTransferDetailId: uint64(item.ProductID),
|
for _, prod := range item.Products {
|
||||||
Quantity: item.ProductQty,
|
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 {
|
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
|
||||||
@@ -225,8 +256,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ type TransferDelivery struct {
|
|||||||
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
|
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
|
||||||
Document string `json:"document"`
|
Document string `json:"document"`
|
||||||
DriverName string `json:"driver_name" validate:"required"`
|
DriverName string `json:"driver_name" validate:"required"`
|
||||||
DeliveryNoteNumber string `json:"delivery_note_number" validate:"required"`
|
|
||||||
VehiclePlate string `json:"vehicle_plate" validate:"required"`
|
VehiclePlate string `json:"vehicle_plate" validate:"required"`
|
||||||
SupplierID uint `json:"supplier_id" validate:"required"`
|
SupplierID uint `json:"supplier_id" validate:"required"`
|
||||||
Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`
|
Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`
|
||||||
|
|||||||
Reference in New Issue
Block a user