From 0a0c3f869bcf394e0b6385e8ec62575140c95a34 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 12 Nov 2025 11:28:18 +0700 Subject: [PATCH] Feat[BE-222,223,224]: creating So create delete patch update get getall approval API --- internal/entities/delivery-orders.go | 23 + internal/entities/marketing.go | 9 +- internal/entities/marketing_product.go | 5 +- internal/entities/sales-orders.go | 3 +- .../marketing-delivery-products.repository.go | 21 + .../controllers/delivery-orders.controller.go | 165 +++++++ .../dto/delivery-orders.dto.go | 164 +++++++ .../marketing/delivery-orderss/module.go | 32 ++ .../delivery-orders.repository.go | 21 + .../marketing/delivery-orderss/route.go | 29 ++ .../services/delivery-orders.service.go | 412 ++++++++++++++++ .../validations/delivery-orders.validation.go | 37 ++ internal/modules/marketing/route.go | 2 + .../controllers/sales-orders.controller.go | 52 +- .../sales-orders/dto/sales-orders.dto.go | 223 ++++++++- .../modules/marketing/sales-orders/module.go | 5 +- .../marketing-delivery-products.repository.go | 21 + .../marketing-products.repository.go | 35 ++ .../repositories/marketings.repository.go | 7 + .../modules/marketing/sales-orders/route.go | 1 + .../services/sales-orders.service.go | 464 +++++++++++++++--- .../validations/sales-orders.validation.go | 13 +- .../users/repositories/user.repository.go | 9 + internal/utils/constant.go | 17 + 24 files changed, 1688 insertions(+), 82 deletions(-) create mode 100644 internal/entities/delivery-orders.go create mode 100644 internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go create mode 100644 internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go create mode 100644 internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go create mode 100644 internal/modules/marketing/delivery-orderss/module.go create mode 100644 internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go create mode 100644 internal/modules/marketing/delivery-orderss/route.go create mode 100644 internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go create mode 100644 internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go create mode 100644 internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go create mode 100644 internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go diff --git a/internal/entities/delivery-orders.go b/internal/entities/delivery-orders.go new file mode 100644 index 00000000..291ba20c --- /dev/null +++ b/internal/entities/delivery-orders.go @@ -0,0 +1,23 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type DeliveryOrders struct { + Id uint `gorm:"primaryKey" json:"id"` + DeliveryNumber string `gorm:"type:varchar(255);not null;uniqueIndex" json:"delivery_number"` + DeliveryDate time.Time `gorm:"not null" json:"delivery_date"` + MarketingId uint `gorm:"not null" json:"marketing_id"` + Notes string `gorm:"type:text" json:"notes"` + CreatedBy uint `gorm:"not null" json:"created_by"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Marketing *Marketing `gorm:"foreignKey:MarketingId;references:Id" json:"marketing,omitempty"` + DeliveryProducts []MarketingDeliveryProduct `gorm:"-" json:"delivery_products,omitempty"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id" json:"created_user,omitempty"` +} diff --git a/internal/entities/marketing.go b/internal/entities/marketing.go index 1ae4d8c3..c9ff7624 100644 --- a/internal/entities/marketing.go +++ b/internal/entities/marketing.go @@ -19,8 +19,9 @@ type Marketing struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Customer Customer `gorm:"foreignKey:CustomerId;references:Id"` - SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"` + Customer Customer `gorm:"foreignKey:CustomerId;references:Id"` + SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"` + LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` } diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index 2e6aef58..f0fe7f38 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -19,6 +19,7 @@ type MarketingProduct struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` - ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` + ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + DeliveryProducts []MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"` } diff --git a/internal/entities/sales-orders.go b/internal/entities/sales-orders.go index 48615607..faa6d901 100644 --- a/internal/entities/sales-orders.go +++ b/internal/entities/sales-orders.go @@ -14,5 +14,6 @@ type SalesOrders struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` } diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go new file mode 100644 index 00000000..95e9b3bb --- /dev/null +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -0,0 +1,21 @@ +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 MarketingDeliveryProductRepository interface { + repository.BaseRepository[entity.MarketingDeliveryProduct] +} + +type MarketingDeliveryProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct] +} + +func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository { + return &MarketingDeliveryProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db), + } +} diff --git a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go new file mode 100644 index 00000000..a5f2839a --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go @@ -0,0 +1,165 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type DeliveryOrdersController struct { + DeliveryOrdersService service.DeliveryOrdersService +} + +func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersService) *DeliveryOrdersController { + return &DeliveryOrdersController{ + DeliveryOrdersService: deliveryOrdersService, + } +} + +func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + MarketingId: uint(c.QueryInt("marketing_id", 0)), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.DeliveryOrdersService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.DeliveryOrdersListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all deliveryOrderss successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: result, + }) +} + +func (u *DeliveryOrdersController) 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.DeliveryOrdersService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get deliveryOrders successfully", + Data: *result, + }) +} + +func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.DeliveryOrdersService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create delivery products successfully", + Data: result, + }) +} + +func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error { + req := new(validation.Update) + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.DeliveryOrdersService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update deliveryOrders successfully", + Data: dto.ToDeliveryOrdersListDTO(*result), + }) +} + +func (u *DeliveryOrdersController) DeleteOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := u.DeliveryOrdersService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete deliveryOrders successfully", + }) +} + +func (u *DeliveryOrdersController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.DeliveryOrdersService.Approval(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Submit delivery order approval successfully", + Data: results, + }) +} diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go new file mode 100644 index 00000000..2b2ea51e --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -0,0 +1,164 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type MarketingDeliveryProductDTO struct { + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` +} + +type DeliveryOrdersBaseDTO struct { + Id uint `json:"id,omitempty"` + DeliveryNumber *string `json:"delivery_number,omitempty"` + DeliveryDate *time.Time `json:"delivery_date,omitempty"` + MarketingId uint `json:"marketing_id"` + Notes string `json:"notes,omitempty"` +} + +type MarketingBaseDTO struct { + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` +} + +type DeliveryOrdersListDTO struct { + DeliveryOrdersBaseDTO + Marketing *MarketingBaseDTO `json:"marketing,omitempty"` + DeliveryProducts []MarketingDeliveryProductDTO `json:"delivery_products,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` +} + +type DeliveryOrdersDetailDTO struct { + DeliveryOrdersListDTO +} + +// === Mapper Functions === + +func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { + return MarketingDeliveryProductDTO{ + Id: e.Id, + MarketingProductId: e.MarketingProductId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + TotalWeight: e.TotalWeight, + AvgWeight: e.AvgWeight, + TotalPrice: e.TotalPrice, + DeliveryDate: e.DeliveryDate, + VehicleNumber: e.VehicleNumber, + } +} + +func ToDeliveryOrdersBaseDTO(e entity.DeliveryOrders) DeliveryOrdersBaseDTO { + var deliveryNumber *string + if e.DeliveryNumber != "" { + deliveryNumber = &e.DeliveryNumber + } + + return DeliveryOrdersBaseDTO{ + Id: e.Id, + DeliveryNumber: deliveryNumber, + DeliveryDate: &e.DeliveryDate, + MarketingId: e.MarketingId, + Notes: e.Notes, + } +} + +func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + + var marketing *MarketingBaseDTO + if e.Marketing != nil && e.Marketing.Id != 0 { + marketing = &MarketingBaseDTO{ + Id: e.Marketing.Id, + SoNumber: e.Marketing.SoNumber, + SoDate: e.Marketing.SoDate, + } + } + + var deliveryProductsDTOs []MarketingDeliveryProductDTO + if len(e.DeliveryProducts) > 0 { + deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(e.DeliveryProducts)) + for i, dp := range e.DeliveryProducts { + deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) + } + } + + return DeliveryOrdersListDTO{ + DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), + Marketing: marketing, + DeliveryProducts: deliveryProductsDTOs, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProducts []entity.MarketingDeliveryProduct) DeliveryOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + + var marketing *MarketingBaseDTO + if e.Marketing != nil && e.Marketing.Id != 0 { + marketing = &MarketingBaseDTO{ + Id: e.Marketing.Id, + SoNumber: e.Marketing.SoNumber, + SoDate: e.Marketing.SoDate, + } + } + + var deliveryProductsDTOs []MarketingDeliveryProductDTO + if len(deliveryProducts) > 0 { + deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(deliveryProducts)) + for i, dp := range deliveryProducts { + deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) + } + } + + return DeliveryOrdersListDTO{ + DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), + Marketing: marketing, + DeliveryProducts: deliveryProductsDTOs, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToDeliveryOrdersListDTOs(e []entity.DeliveryOrders) []DeliveryOrdersListDTO { + result := make([]DeliveryOrdersListDTO, len(e)) + for i, r := range e { + result[i] = ToDeliveryOrdersListDTO(r) + } + return result +} + +func ToDeliveryOrdersDetailDTO(e entity.DeliveryOrders) DeliveryOrdersDetailDTO { + return DeliveryOrdersDetailDTO{ + DeliveryOrdersListDTO: ToDeliveryOrdersListDTO(e), + } +} diff --git a/internal/modules/marketing/delivery-orderss/module.go b/internal/modules/marketing/delivery-orderss/module.go new file mode 100644 index 00000000..c6932c51 --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/module.go @@ -0,0 +1,32 @@ +package delivery_orderss + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + rDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" + sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type DeliveryOrdersModule struct{} + +func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + deliveryOrdersRepo := rDeliveryOrders.NewDeliveryOrdersRepository(db) + marketingRepo := rMarketing.NewMarketingRepository(db) + marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db) + userRepo := rUser.NewUserRepository(db) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalSvc := commonSvc.NewApprovalService(approvalRepo) + + deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingDeliveryProductRepo, approvalSvc, validate) + userService := sUser.NewUserService(userRepo, validate) + + DeliveryOrdersRoutes(router, userService, deliveryOrdersService) +} diff --git a/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go b/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go new file mode 100644 index 00000000..0a7d7f3d --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type DeliveryOrdersRepository interface { + repository.BaseRepository[entity.DeliveryOrders] +} + +type DeliveryOrdersRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.DeliveryOrders] +} + +func NewDeliveryOrdersRepository(db *gorm.DB) DeliveryOrdersRepository { + return &DeliveryOrdersRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.DeliveryOrders](db), + } +} diff --git a/internal/modules/marketing/delivery-orderss/route.go b/internal/modules/marketing/delivery-orderss/route.go new file mode 100644 index 00000000..8d58b70e --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/route.go @@ -0,0 +1,29 @@ +package delivery_orderss + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers" + deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) { + ctrl := controller.NewDeliveryOrdersController(s) + + route := v1.Group("/delivery-orders") + + // 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) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) + route.Post("/approvals", ctrl.Approval) +} diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go new file mode 100644 index 00000000..db238d0f --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -0,0 +1,412 @@ +package service + +import ( + "errors" + "fmt" + "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" + marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" + marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/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 DeliveryOrdersService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) + DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) +} + +type deliveryOrdersService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.DeliveryOrdersRepository + MarketingRepo marketingRepo.MarketingRepository + MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository + ApprovalSvc commonSvc.ApprovalService +} + +func NewDeliveryOrdersService( + repo repository.DeliveryOrdersRepository, + marketingRepo marketingRepo.MarketingRepository, + marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository, + approvalSvc commonSvc.ApprovalService, + validate *validator.Validate, +) DeliveryOrdersService { + return &deliveryOrdersService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + MarketingRepo: marketingRepo, + MarketingDeliveryProductRepo: marketingDeliveryProductRepo, + ApprovalSvc: approvalSvc, + } +} + +func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser"). + Preload("Marketing") +} + +func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + // Fetch dari Marketing, bukan DeliveryOrders + marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse") + if params.MarketingId != 0 { + return db.Where("id = ?", params.MarketingId) + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get marketings: %+v", err) + return nil, 0, err + } + + // Load delivery products untuk setiap marketing + result := make([]dto.DeliveryOrdersListDTO, len(marketings)) + for i, marketing := range marketings { + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", marketing.Id)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) + // Continue without products + } + + // Create dummy DeliveryOrders untuk dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: marketing.Id, + CreatedUser: &marketing.CreatedUser, + Marketing: &marketing, + DeliveryProducts: deliveryProducts, + } + + result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + } + + return result, total, nil +} + +func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) { + // Fetch Marketing by ID, bukan DeliveryOrders + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse") + }) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + if err != nil { + s.Log.Errorf("Failed get marketing by id: %+v", err) + return nil, err + } + + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", marketing.Id)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) + // Continue without products + } + + // Create dummy DeliveryOrders untuk dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: marketing.Id, + CreatedUser: &marketing.CreatedUser, + Marketing: marketing, + DeliveryProducts: deliveryProducts, + } + + result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + return &result, nil +} + +func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + // Validate marketing exists + _, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + if err != nil { + s.Log.Errorf("Failed to fetch marketing %d: %+v", req.MarketingId, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } + + // Validate marketing approval status - harus sudah di approve ke step Sales Order + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval") + } + + // Cek apakah status approval sudah Sales Order (step 2) atau lebih + if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") + } + + if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing is not approved for delivery") + } + + // Validate semua delivery products ada dan update mereka + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { + for _, product := range req.DeliveryProducts { + // Fetch marketing_product terlebih dahulu untuk pastikan punya marketing_id yang sama + var marketingProduct entity.MarketingProduct + if err := dbTx.Where("id = ? AND marketing_id = ?", product.MarketingProductId, req.MarketingId). + First(&marketingProduct).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", product.MarketingProductId)) + } + s.Log.Errorf("Failed to fetch marketing product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing product") + } + + // Fetch marketing_delivery_product by marketing_product_id + var mdp entity.MarketingDeliveryProduct + if err := dbTx.Where("marketing_product_id = ?", marketingProduct.Id). + First(&mdp).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", product.MarketingProductId)) + } + s.Log.Errorf("Failed to fetch marketing delivery product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") + } + + // Parse delivery date per item (jika ada), atau gunakan default + itemDeliveryDate := time.Now() + if product.DeliveryDate != "" { + parsedItemDate, err := time.Parse("2006-01-02", product.DeliveryDate) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d", product.MarketingProductId)) + } + itemDeliveryDate = parsedItemDate + } + + // Update dengan data dari request - PASTIKAN UPDATE LANGSUNG KE FIELD + updates := map[string]interface{}{ + "qty": product.Qty, + "unit_price": product.UnitPrice, + "avg_weight": product.AvgWeight, + "total_weight": product.TotalWeight, + "total_price": product.TotalPrice, + "delivery_date": &itemDeliveryDate, + "vehicle_number": product.VehicleNumber, + } + + if err := dbTx.Model(&mdp).Updates(updates).Error; err != nil { + s.Log.Errorf("Failed to update marketing delivery product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + } + + s.Log.Infof("Updated MDP %d: qty=%v, unitPrice=%v, totalPrice=%v", mdp.Id, product.Qty, product.UnitPrice, product.TotalPrice) + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Fetch marketing dengan delivery products yang sudah di-update + marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser").Preload("Products") + }) + if err != nil { + s.Log.Errorf("Failed to fetch marketing after update: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") + } + + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", req.MarketingId)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products: %+v", err) + // Continue tanpa delivery products + } + + // Create dummy DeliveryOrders untuk dipakai dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: req.MarketingId, + Notes: req.Notes, + CreatedUser: &marketing.CreatedUser, + Marketing: marketing, + DeliveryProducts: deliveryProducts, + } + + result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + return &result, nil +} + +func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.DeliveryDate != "" { + deliveryDate, err := time.Parse("2006-01-02", req.DeliveryDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid delivery date format") + } + updateBody["delivery_date"] = deliveryDate + } + + if req.Notes != "" { + updateBody["notes"] = req.Notes + } + + if len(updateBody) == 0 { + return s.Repository.GetByID(c.Context(), id, s.withRelations) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + } + s.Log.Errorf("Failed to update deliveryOrders: %+v", err) + return nil, err + } + + return s.Repository.GetByID(c.Context(), id, s.withRelations) +} + +func (s deliveryOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + var action entity.ApprovalAction + switch req.Action { + case "APPROVED": + action = entity.ApprovalActionApproved + case "REJECTED": + action = entity.ApprovalActionRejected + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + // Validate semua delivery order ada + for _, id := range approvableIDs { + _, err := s.Repository.GetByID(c.Context(), id, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) + } + if err != nil { + s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) + } + } + + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTx)) + + for _, approvableID := range approvableIDs { + actorID := uint(1) // TODO: get from auth context + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + approvableID, + utils.MarketingDeliveryOrder, + &action, + actorID, + req.Notes, + ); err != nil { + s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") + } + } + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + updated := make([]entity.DeliveryOrders, 0, len(approvableIDs)) + for _, id := range approvableIDs { + deliveryOrder, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) + } + s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) + } + updated = append(updated, *deliveryOrder) + } + + return updated, nil +} + +func (s deliveryOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + } + s.Log.Errorf("Failed to delete deliveryOrders: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go new file mode 100644 index 00000000..a80ad8d5 --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go @@ -0,0 +1,37 @@ +package validation + +type DeliveryProduct struct { + MarketingProductId uint `json:"marketing_product_id" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"required,gt=0"` + UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` + TotalPrice float64 `json:"total_price" validate:"required,gt=0"` + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"` +} + +type Create struct { + MarketingId uint `json:"marketing_id" validate:"required,gt=0"` + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"` + Notes string `json:"notes" validate:"omitempty,max=500"` +} + +type Update struct { + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"` + Notes string `json:"notes" validate:"omitempty,max=500"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"` +} + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go index 1b72b8cb..1ab03896 100644 --- a/internal/modules/marketing/route.go +++ b/internal/modules/marketing/route.go @@ -8,6 +8,7 @@ import ( "gorm.io/gorm" salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders" + deliveryOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss" // MODULE IMPORTS ) @@ -16,6 +17,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida allModules := []modules.Module{ salesOrderss.SalesOrdersModule{}, + deliveryOrderss.DeliveryOrdersModule{}, // MODULE REGISTRY } diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index be578e15..4c49baeb 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -38,6 +38,12 @@ func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { return err } + // Convert marketing data to sales orders DTOs with products + salesOrdersDTOs := make([]dto.SalesOrdersListDTO, len(result)) + for i, marketing := range result { + salesOrdersDTOs[i] = dto.ToSalesOrdersListDTOFromMarketing(marketing) + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{ Code: fiber.StatusOK, @@ -49,7 +55,7 @@ func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToSalesOrdersListDTOs(result), + Data: salesOrdersDTOs, }) } @@ -71,7 +77,7 @@ func (u *SalesOrdersController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get salesOrders successfully", - Data: dto.ToSalesOrdersListDTO(*result), + Data: dto.ToSalesOrdersListDTOFromMarketing(*result), }) } @@ -109,7 +115,13 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id)) + _, err = u.SalesOrdersService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + // Fetch full updated data for response + result, err := u.SalesOrdersService.GetOne(c, uint(id)) if err != nil { return err } @@ -119,7 +131,7 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update salesOrders successfully", - Data: dto.ToSalesOrdersListDTO(*result), + Data: dto.ToSalesOrdersListDTOFromMarketing(*result), }) } @@ -142,3 +154,35 @@ func (u *SalesOrdersController) DeleteOne(c *fiber.Ctx) error { Message: "Delete salesOrders successfully", }) } + +func (u *SalesOrdersController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.SalesOrdersService.Approval(c, req) + if err != nil { + return err + } + + var ( + data interface{} + message = "Submit sales order approval successfully" + ) + if len(results) == 1 { + data = dto.ToSalesOrdersListDTOFromMarketing(results[0]) + } else { + message = "Submit sales order approvals successfully" + data = dto.ToSalesOrdersListDTOsFromMarketing(results) + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: message, + Data: data, + }) +} diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 6dd7c8e3..920b8d26 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -4,21 +4,60 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" ) // === DTO Structs === type SalesOrdersBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + Name string `json:"name"` +} + +type MarketingProductDTO struct { + Id uint `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + } `json:"product_warehouse,omitempty"` +} + +type MarketingDeliveryProductDTO struct { + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` } type SalesOrdersListDTO struct { SalesOrdersBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CustomerId uint `json:"customer_id,omitempty"` + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SoDate *time.Time `json:"so_date,omitempty"` + SalesPersonId uint `json:"sales_person_id,omitempty"` + Notes string `json:"notes,omitempty"` + MarketingProducts []MarketingProductDTO `json:"marketing_products,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval approvalDTO.ApprovalBaseDTO `json:"approval"` } type SalesOrdersDetailDTO struct { @@ -29,11 +68,92 @@ type SalesOrdersDetailDTO struct { func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO { return SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.Name, + Id: e.Id, + Name: e.Name, } } +func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { + var productWarehouse *struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + } + + if e.ProductWarehouse.Id != 0 { + product := (*productDTO.ProductBaseDTO)(nil) + if e.ProductWarehouse.Product.Id != 0 { + mapped := productDTO.ToProductBaseDTO(e.ProductWarehouse.Product) + product = &mapped + } + + warehouse := (*warehouseDTO.WarehouseBaseDTO)(nil) + if e.ProductWarehouse.Warehouse.Id != 0 { + mapped := warehouseDTO.ToWarehouseBaseDTO(e.ProductWarehouse.Warehouse) + warehouse = &mapped + } + + productWarehouse = &struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + }{ + Id: e.ProductWarehouse.Id, + Product: product, + Warehouse: warehouse, + } + } + + return MarketingProductDTO{ + Id: e.Id, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + ProductWarehouse: productWarehouse, + } +} + +func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { + return MarketingDeliveryProductDTO{ + Id: e.Id, + MarketingProductId: e.MarketingProductId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + TotalWeight: e.TotalWeight, + AvgWeight: e.AvgWeight, + TotalPrice: e.TotalPrice, + DeliveryDate: e.DeliveryDate, + VehicleNumber: e.VehicleNumber, + } +} + +func defaultSalesOrdersLatestApproval(e entity.SalesOrders) approvalDTO.ApprovalBaseDTO { + result := approvalDTO.ApprovalBaseDTO{} + + step := utils.MarketingStepPengajuan + if step > 0 { + result.StepNumber = uint16(step) + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { + result.StepName = label + } else if label, ok := utils.MarketingApprovalSteps[step]; ok { + result.StepName = label + } + } + + if e.CreatedUser.Id != 0 { + result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) + } else if e.CreatedBy != 0 { + result.ActionBy = userDTO.UserBaseDTO{ + Id: e.CreatedBy, + IdUser: int64(e.CreatedBy), + } + } + + return result +} + func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { @@ -41,14 +161,97 @@ func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { createdUser = &mapped } + approval := defaultSalesOrdersLatestApproval(e) + if e.LatestApproval != nil { + approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) + } + return SalesOrdersListDTO{ SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: approval, } } +func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + var marketingProducts []MarketingProductDTO + if len(e.Products) > 0 { + marketingProducts = make([]MarketingProductDTO, len(e.Products)) + for i, product := range e.Products { + marketingProducts[i] = ToMarketingProductDTO(product) + } + } + + var customerSummary *customerDTO.CustomerBaseDTO + if e.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(e.Customer) + customerSummary = &mapped + } + + approval := defaultSalesOrdersLatestApprovalFromMarketing(e) + if e.LatestApproval != nil { + approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) + } + + return SalesOrdersListDTO{ + SalesOrdersBaseDTO: SalesOrdersBaseDTO{ + Id: e.Id, + Name: e.SoNumber, + }, + CustomerId: e.Customer.Id, + Customer: customerSummary, + SoDate: &e.SoDate, + SalesPersonId: e.SalesPersonId, + Notes: e.Notes, + MarketingProducts: marketingProducts, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: approval, + } +} + +func defaultSalesOrdersLatestApprovalFromMarketing(e entity.Marketing) approvalDTO.ApprovalBaseDTO { + result := approvalDTO.ApprovalBaseDTO{} + + step := utils.MarketingStepPengajuan + if step > 0 { + result.StepNumber = uint16(step) + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { + result.StepName = label + } else if label, ok := utils.MarketingApprovalSteps[step]; ok { + result.StepName = label + } + } + + if e.CreatedUser.Id != 0 { + result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) + } else if e.CreatedBy != 0 { + result.ActionBy = userDTO.UserBaseDTO{ + Id: e.CreatedBy, + IdUser: int64(e.CreatedBy), + } + } + + return result +} + +func ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListDTO { + result := make([]SalesOrdersListDTO, len(e)) + for i, r := range e { + result[i] = ToSalesOrdersListDTOFromMarketing(r) + } + return result +} + func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO { result := make([]SalesOrdersListDTO, len(e)) for i, r := range e { diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 21926f93..35d002fc 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -5,6 +5,8 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" @@ -25,7 +27,8 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) marketingRepo := rSalesOrders.NewMarketingRepository(db) - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, validate) + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) diff --git a/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go b/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go new file mode 100644 index 00000000..95e9b3bb --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go @@ -0,0 +1,21 @@ +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 MarketingDeliveryProductRepository interface { + repository.BaseRepository[entity.MarketingDeliveryProduct] +} + +type MarketingDeliveryProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct] +} + +func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository { + return &MarketingDeliveryProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db), + } +} diff --git a/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go new file mode 100644 index 00000000..d3a6798f --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go @@ -0,0 +1,35 @@ +package repository + +import ( + "context" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type MarketingProductRepository interface { + repository.BaseRepository[entity.MarketingProduct] + GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) +} + +type MarketingProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingProduct] +} + +func NewMarketingProductRepository(db *gorm.DB) MarketingProductRepository { + return &MarketingProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingProduct](db), + } +} + +func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) { + var products []entity.MarketingProduct + if err := r.DB().WithContext(ctx).Where("marketing_id = ?", marketingID).Find(&products).Error; err != nil { + return nil, err + } + if len(products) == 0 { + return products, gorm.ErrRecordNotFound + } + return products, nil +} diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go index bb4896cb..f06bf401 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,6 +10,7 @@ import ( type MarketingRepository interface { repository.BaseRepository[entity.Marketing] + IdExists(ctx context.Context, id uint) (bool, error) } type MarketingRepositoryImpl struct { @@ -19,3 +22,7 @@ func NewMarketingRepository(db *gorm.DB) MarketingRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db), } } + +func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Marketing](ctx, r.DB(), id) +} diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go index 455977fb..c48ae2a7 100644 --- a/internal/modules/marketing/sales-orders/route.go +++ b/internal/modules/marketing/sales-orders/route.go @@ -25,4 +25,5 @@ func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesO route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 0a1e5898..67163564 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -2,8 +2,11 @@ package service import ( "errors" + "fmt" + "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" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" @@ -11,7 +14,9 @@ import ( validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -20,11 +25,12 @@ import ( ) type SalesOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.SalesOrders, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Marketing, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) } type salesOrdersService struct { @@ -35,9 +41,11 @@ type salesOrdersService struct { KandangRepo kandangRepo.KandangRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository MarketingRepo repository.MarketingRepository + UserRepo userRepo.UserRepository + ApprovalSvc commonSvc.ApprovalService } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, @@ -46,48 +54,94 @@ func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo c KandangRepo: kandangRepo, ProductWarehouseRepo: productWarehouseRepo, MarketingRepo: marketingRepo, + UserRepo: userRepo, + ApprovalSvc: approvalSvc, } } func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse"). + Preload("Products.DeliveryProducts") } -func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) { +func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit - salesOrderss, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.Search != "" { - return db.Where("name LIKE ?", "%"+params.Search+"%") + return db.Where("so_number LIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { - s.Log.Errorf("Failed to get salesOrderss: %+v", err) - return nil, 0, err + s.Log.Errorf("Failed to get marketings: %+v", err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales orders") } - return salesOrderss, total, nil + + if s.ApprovalSvc != nil && len(marketings) > 0 { + ids := make([]uint, len(marketings)) + for i, item := range marketings { + ids[i] = item.Id + } + + latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load latest approvals for marketings: %+v", err) + } else if len(latestMap) > 0 { + for i := range marketings { + if approval, ok := latestMap[marketings[i].Id]; ok { + marketings[i].LatestApproval = approval + } + } + } + } + + return marketings, total, nil } -func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.SalesOrders, error) { - salesOrders, err := s.Repository.GetByID(c.Context(), id, s.withRelations) +func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { + + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } if err != nil { - s.Log.Errorf("Failed get salesOrders by id: %+v", err) - return nil, err + s.Log.Errorf("Failed get marketing by id: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } - return salesOrders, nil + + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load approvals for marketing %d: %+v", id, err) + } else if len(approvals) > 0 { + if marketing.LatestApproval == nil { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest + } + } else { + marketing.LatestApproval = nil + } + } + + return marketing, nil } -func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) { +func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -107,25 +161,26 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } } - // parse date soDate, err := utils.ParseDateString(req.Date) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - // generate SO number soNumber := "SO-" + time.Now().Format("20060102150405") var marketing *entity.Marketing - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - // create marketing directly (tanpa create SalesOrders) + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + marketing = &entity.Marketing{ CustomerId: req.CustomerId, SoNumber: soNumber, SoDate: soDate, - SalesPersonId: 1, + SalesPersonId: req.SalesPersonId, Notes: req.Notes, CreatedBy: 1, } @@ -134,7 +189,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return err } - // Create MarketingProducts and MarketingDeliveryProducts if len(req.MarketingProducts) > 0 { for _, product := range req.MarketingProducts { marketingProduct := &entity.MarketingProduct{ @@ -146,12 +200,11 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e TotalWeight: product.TotalWeight, TotalPrice: product.TotalPrice, } - if err := dbTransaction.Create(marketingProduct).Error; err != nil { + if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { s.Log.Errorf("Failed to create marketing product: %+v", err) return err } - // create delivery product with zeroed numeric fields and nil delivery date marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ MarketingProductId: marketingProduct.Id, Qty: 0, @@ -162,13 +215,37 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e DeliveryDate: nil, VehicleNumber: product.VehicleNumber, } - if err := dbTransaction.Create(marketingDeliveryProduct).Error; err != nil { + if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { s.Log.Errorf("Failed to create marketing delivery product: %+v", err) return err } } } + actorID := uint(1) // TODO: ambil dari auth context + if err := approvalSvcTx.RegisterWorkflowSteps( + utils.ApprovalWorkflowMarketing, + utils.MarketingApprovalSteps, + ); err != nil { + s.Log.Errorf("Failed to register workflow steps: %+v", err) + return err + } + + approvalAction := entity.ApprovalActionCreated + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + marketing.Id, + utils.MarketingStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + s.Log.Errorf("Failed to create approval: %+v", err) + return err + } + } + return nil }) if err != nil { @@ -178,51 +255,320 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } - createdMarketing, err := s.MarketingRepo.GetByID(c.Context(), marketing.Id, nil) - if err != nil { - s.Log.Errorf("Failed to fetch created marketing: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created marketing") - } - - return &entity.SalesOrders{ - Id: createdMarketing.Id, - Name: createdMarketing.SoNumber, - }, nil + return s.GetOne(c, marketing.Id) } -func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) { +func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - updateBody := make(map[string]any) - - if req.Name != nil { - updateBody["name"] = *req.Name - } - - if len(updateBody) == 0 { - return s.GetOne(c, id) - } - - if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") - } - s.Log.Errorf("Failed to update salesOrders: %+v", 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}, + commonSvc.RelationCheck{Name: "SalesPerson", ID: &req.SalesPersonId, Exists: s.UserRepo.IdExists}, + ); err != nil { return nil, err } + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) + if err != nil { + s.Log.Errorf("Failed to check approval status: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + if latestApproval != nil && latestApproval.StepNumber >= 3 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Cannot update sales order after delivery order approval") + } + + if len(req.MarketingProducts) > 0 { + for _, item := range req.MarketingProducts { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err + } + } + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + updateBody := make(map[string]any) + if req.CustomerId != 0 { + updateBody["customer_id"] = req.CustomerId + } + if req.SalesPersonId != 0 { + updateBody["sales_person_id"] = req.SalesPersonId + } + if req.Date != "" { + soDate, err := utils.ParseDateString(req.Date) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid date format") + } + updateBody["so_date"] = soDate + } + if req.Notes != "" { + updateBody["notes"] = req.Notes + } + + if len(updateBody) > 0 { + if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") + } + } + + if len(req.MarketingProducts) > 0 { + + oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id) + if err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to fetch old marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + + for _, oldProduct := range oldProducts { + if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_product_id = ?", oldProduct.Id) + }); err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to delete delivery products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + } + + if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_id = ?", id) + }); err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to delete marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + + for _, product := range req.MarketingProducts { + marketingProduct := &entity.MarketingProduct{ + MarketingId: id, + ProductWarehouseId: product.ProductWarehouseId, + Qty: product.Qty, + UnitPrice: product.UnitPrice, + AvgWeight: product.AvgWeight, + TotalWeight: product.TotalWeight, + TotalPrice: product.TotalPrice, + } + if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { + + return err + } + + marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ + MarketingProductId: marketingProduct.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: product.VehicleNumber, + } + if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { + return err + } + } + } + + if latestApproval != nil && latestApproval.StepNumber == 2 { + actorID := uint(1) // todo: ambil dari auth context + resetNote := "" + action := entity.ApprovalActionApproved + _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + id, + utils.MarketingStepPengajuan, + &action, + actorID, + &resetNote) + if err != nil { + s.Log.Errorf("Failed to create reset approval: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") + } + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") + } + return s.GetOne(c, id) } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") - } - s.Log.Errorf("Failed to delete salesOrders: %+v", err) - return err + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } + if err != nil { + s.Log.Errorf("Failed to fetch marketing %d before delete: %+v", id, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + + if len(marketing.Products) > 0 { + for _, product := range marketing.Products { + if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_product_id = ?", product.Id) + }); err != nil && err != gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") + } + } + } + + if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_id = ?", id) + }); err != nil && err != gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") + } + + if err := marketingRepoTx.DeleteOne(c.Context(), id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order") + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return fiberErr + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order") + } + return nil } + +func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + + var action entity.ApprovalAction + switch strings.ToUpper(strings.TrimSpace(req.Action)) { + case string(entity.ApprovalActionRejected): + action = entity.ApprovalActionRejected + case string(entity.ApprovalActionApproved): + action = entity.ApprovalActionApproved + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + for _, id := range approvableIDs { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, + ); err != nil { + return nil, err + } + + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id)) + } + + if action == entity.ApprovalActionApproved { + switch latestApproval.StepNumber { + case uint16(utils.MarketingStepPengajuan): + case uint16(utils.MarketingStepSalesOrder): + default: + return nil, fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber)) + } + } + } + + err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + for _, approvableID := range approvableIDs { + + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil) + if err != nil { + s.Log.Errorf("Failed to get latest approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step") + } + + if latestApproval == nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", approvableID)) + } + + var nextStep approvalutils.ApprovalStep + currentStep := latestApproval.StepNumber + + if action == entity.ApprovalActionApproved { + + if currentStep == uint16(utils.MarketingStepPengajuan) { + nextStep = utils.MarketingStepSalesOrder + } else if currentStep == uint16(utils.MarketingStepSalesOrder) { + nextStep = utils.MarketingDeliveryOrder + } else { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d already completed all approval steps", approvableID)) + } + } else { + + nextStep = approvalutils.ApprovalStep(currentStep) + } + + actorID := uint(1) // todo ambil dari auth context + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + approvableID, + nextStep, + &action, + actorID, + req.Notes, + ); err != nil { + s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") + } + } + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + updated := make([]entity.Marketing, 0, len(approvableIDs)) + for _, id := range approvableIDs { + marketing, err := s.GetOne(c, id) + if err != nil { + return nil, err + } + updated = append(updated, *marketing) + } + + return updated, nil +} diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go index b09129bc..01b3af9d 100644 --- a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -2,6 +2,7 @@ package validation type Create struct { CustomerId uint `json:"customer_id" validate:"required,gt=0"` + SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"` Date string `json:"date" validate:"required,datetime=2006-01-02"` Notes string `json:"notes" validate:"omitempty,max=500"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"` @@ -19,7 +20,11 @@ type CreateMarketingProduct struct { } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + CustomerId uint `json:"customer_id" validate:"omitempty,gt=0"` + SalesPersonId uint `json:"sales_person_id" validate:"omitempty,gt=0"` + Date string `json:"date" validate:"omitempty,datetime=2006-01-02"` + Notes string `json:"notes" validate:"omitempty,max=500"` + MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"` } type Query struct { @@ -27,3 +32,9 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Search string `query:"search" validate:"omitempty,max=50"` } + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/users/repositories/user.repository.go b/internal/modules/users/repositories/user.repository.go index 8472db13..28c06a74 100644 --- a/internal/modules/users/repositories/user.repository.go +++ b/internal/modules/users/repositories/user.repository.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,14 +10,21 @@ import ( type UserRepository interface { repository.BaseRepository[entity.User] + IdExists(ctx context.Context, id uint) (bool, error) } type UserRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.User] + db *gorm.DB } func NewUserRepository(db *gorm.DB) UserRepository { return &UserRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.User](db), + db: db, } } + +func (r *UserRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.User](ctx, r.db, id) +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 099b6510..fc01a231 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -224,6 +224,23 @@ var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{ PurchaseStepStaffPurchase: "Staff Purchase", } +// ------------------------------------------------------------------- +// Marketings Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowMarketing approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("MARKETINGS") + MarketingStepPengajuan approvalutils.ApprovalStep = 1 + MarketingStepSalesOrder approvalutils.ApprovalStep = 2 + MarketingDeliveryOrder approvalutils.ApprovalStep = 3 +) + +var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{ + MarketingStepPengajuan: "Pengajuan", + MarketingStepSalesOrder: "Sales Order", + MarketingDeliveryOrder: "Delivery Order", +} + // ------------------------------------------------------------------- // Validators // -------------------------------------------------------------------