feat: Add client module with CRUD operations, DTOs, repository, and service layers
Deploy Application / deploy (push) Successful in 23s Details

This commit is contained in:
Habib Fatkhul Rohman 2025-10-28 10:09:31 +07:00
parent f9c5dc3f63
commit 2db31dc017
10 changed files with 610 additions and 3 deletions

View File

@ -8,6 +8,7 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/docs"
"github.com/Caknoooo/go-gin-clean-starter/middlewares"
"github.com/Caknoooo/go-gin-clean-starter/modules/auth"
"github.com/Caknoooo/go-gin-clean-starter/modules/client"
maintenancegroup "github.com/Caknoooo/go-gin-clean-starter/modules/maintenance_group"
"github.com/Caknoooo/go-gin-clean-starter/modules/menu"
"github.com/Caknoooo/go-gin-clean-starter/modules/role"
@ -124,6 +125,7 @@ func main() {
logs.RegisterRoutes(server, injector)
menu.RegisterRoutes(server, injector)
maintenancegroup.RegisterRoutes(server, injector)
client.RegisterRoutes(server, injector)
// register swagger route
server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

View File

@ -9,7 +9,7 @@ type M_Client struct {
Phone string `gorm:"type:varchar(20)" json:"phone"`
Email string `gorm:"type:varchar(100)" json:"email"`
Address string `gorm:"type:text" json:"address"`
LogoUrl string `gorm:"type:varchar(255)" json:"logo_url"`
Logo string `gorm:"type:text" json:"logo"` // simpan base64 string hasil konversi file gambar
Users []M_User `gorm:"foreignKey:ClientID;references:ID" json:"users"`
MaintenanceGroups []M_MaintenanceGroup `gorm:"foreignKey:ClientID;references:ID" json:"maintenance_groups"`

View File

@ -62,7 +62,7 @@ func ListClientSeeder(db *gorm.DB) error {
Phone: data.Phone,
Email: data.Email,
Address: data.Address,
LogoUrl: data.LogoUrl,
// Logo: data.LogoUrl,
}).Error; err != nil {
fmt.Println("Error seeding client:", err)
return err

View File

@ -0,0 +1,149 @@
package controller
import (
"encoding/base64"
"io"
"net/http"
"github.com/Caknoooo/go-gin-clean-starter/modules/client/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/client/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/client/service"
"github.com/Caknoooo/go-gin-clean-starter/pkg/constants"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/Caknoooo/go-pagination"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"gorm.io/gorm"
)
type (
ClientController interface {
Create(ctx *gin.Context)
Update(ctx *gin.Context)
Delete(ctx *gin.Context)
GetById(ctx *gin.Context)
GetAll(ctx *gin.Context)
}
clientController struct {
clientService service.ClientService
db *gorm.DB
}
)
func NewClientController(i *do.Injector, clientService service.ClientService) ClientController {
db := do.MustInvokeNamed[*gorm.DB](i, constants.DB)
return &clientController{
clientService: clientService,
db: db,
}
}
func (c *clientController) Create(ctx *gin.Context) {
var req dto.ClientCreateRequest
if err := ctx.ShouldBind(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
// Ambil file logo (opsional)
file, _, err := ctx.Request.FormFile("logo")
if err == nil && file != nil {
fileBytes, _ := io.ReadAll(file)
base64Str := base64.StdEncoding.EncodeToString(fileBytes)
req.Logo = base64Str
}
created, err := c.clientService.Create(ctx, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_CLIENT, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_CLIENT, created)
ctx.JSON(http.StatusOK, res)
}
func (c *clientController) Update(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.ClientUpdateRequest
if err := ctx.ShouldBind(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
// Ambil file logo (opsional, untuk update)
file, _, err := ctx.Request.FormFile("logo")
if err == nil && file != nil {
fileBytes, _ := io.ReadAll(file)
base64Str := base64.StdEncoding.EncodeToString(fileBytes)
req.Logo = &base64Str
}
client, err := c.clientService.Update(ctx, req, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_CLIENT, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_CLIENT, client)
ctx.JSON(http.StatusOK, res)
}
func (c *clientController) Delete(ctx *gin.Context) {
id := ctx.Param("id")
if err := c.clientService.Delete(ctx, id); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_CLIENT, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_CLIENT, nil)
ctx.JSON(http.StatusOK, res)
}
func (c *clientController) GetById(ctx *gin.Context) {
id := ctx.Param("id")
client, err := c.clientService.GetById(ctx, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CLIENT, err.Error(), nil)
ctx.JSON(http.StatusNotFound, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_CLIENT, client)
ctx.JSON(http.StatusOK, res)
}
func (c *clientController) GetAll(ctx *gin.Context) {
// clientId := ctx.MustGet("client_id").(string)
var filter = &query.ClientFilter{
Name: ctx.Query("name"),
PIC: ctx.Query("pic"),
Phone: ctx.Query("phone"),
Email: ctx.Query("email"),
Address: ctx.Query("address"),
// ClientID: clientId,
Includes: ctx.QueryArray("includes"),
}
filter.BindPagination(ctx)
ctx.ShouldBindQuery(filter)
groups, total, err := pagination.PaginatedQueryWithIncludableAndOptions[query.M_Client](
c.db,
filter,
pagination.PaginatedQueryOptions{
EnableSoftDelete: true,
},
)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CLIENT, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
paginationResponse := pagination.CalculatePagination(filter.Pagination, total)
response := pagination.NewPaginatedResponse(http.StatusOK, dto.MESSAGE_SUCCESS_GET_CLIENT, groups, paginationResponse)
ctx.JSON(http.StatusOK, response)
}

View File

@ -0,0 +1,54 @@
package dto
import (
"errors"
)
const (
MESSAGE_FAILED_CREATE_CLIENT = "failed create client"
MESSAGE_SUCCESS_CREATE_CLIENT = "success create client"
MESSAGE_FAILED_GET_CLIENT = "failed get client"
MESSAGE_SUCCESS_GET_CLIENT = "success get client"
MESSAGE_FAILED_UPDATE_CLIENT = "failed update client"
MESSAGE_SUCCESS_UPDATE_CLIENT = "success update client"
MESSAGE_FAILED_DELETE_CLIENT = "failed delete client"
MESSAGE_SUCCESS_DELETE_CLIENT = "success delete client"
MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body"
)
var (
ErrCreateClient = errors.New("failed to create client")
ErrGetClientById = errors.New("failed to get client by id")
ErrUpdateClient = errors.New("failed to update client")
ErrDeleteClient = errors.New("failed to delete client")
)
type (
ClientCreateRequest struct {
Name string `json:"name" form:"name" binding:"required,min=2,max=100"`
PIC string `json:"pic" form:"pic" binding:"omitempty,min=2,max=100"`
Phone string `json:"phone" form:"phone" binding:"omitempty,min=6,max=20"`
Email string `json:"email" form:"email" binding:"omitempty,email,max=100"`
Address string `json:"address" form:"address" binding:"omitempty"`
Logo string `form:"-"` // diisi manual dari file, tidak di-bind otomatis
}
ClientResponse struct {
ID string `json:"id"`
Name string `json:"name"`
PIC string `json:"pic"`
Phone string `json:"phone"`
Email string `json:"email"`
Address string `json:"address"`
Logo string `json:"logo"`
}
ClientUpdateRequest struct {
Name *string `json:"name" form:"name" binding:"omitempty,min=2,max=100"`
PIC *string `json:"pic" form:"pic" binding:"omitempty,min=2,max=100"`
Phone *string `json:"phone" form:"phone" binding:"omitempty,min=6,max=20"`
Email *string `json:"email" form:"email" binding:"omitempty,email,max=100"`
Address *string `json:"address" form:"address" binding:"omitempty"`
Logo *string `form:"-"` // jika update logo, isi manual dari file
}
)

View File

@ -0,0 +1,96 @@
package query
import (
"github.com/Caknoooo/go-pagination"
"gorm.io/gorm"
)
type M_Client struct {
ID string `json:"id"`
Name string `json:"name"`
PIC string `json:"pic"`
Phone string `json:"phone"`
Email string `json:"email"`
Address string `json:"address"`
Logo string `json:"logo"`
}
type ClientFilter struct {
pagination.BaseFilter
Name string `form:"name"` // tambahkan ini
PIC string `form:"pic"` // tambahkan ini
Phone string `form:"phone"` // tambahkan ini
Email string `form:"email"` // tambahkan ini
Address string `form:"address"` // tambahkan ini
ClientID string `form:"client_id"` // tambahkan ini
Includes []string `form:"includes"` // tambahkan ini
}
func (f *ClientFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
// Apply your filters here
if f.Name != "" {
query = query.Where("name ILIKE ?", "%"+f.Name+"%")
}
if f.ClientID != "" {
query = query.Where("client_id = ?", f.ClientID)
}
if f.PIC != "" {
query = query.Where("pic ILIKE ?", "%"+f.PIC+"%")
}
if f.Phone != "" {
query = query.Where("phone ILIKE ?", "%"+f.Phone+"%")
}
if f.Email != "" {
query = query.Where("email ILIKE ?", "%"+f.Email+"%")
}
if f.Address != "" {
query = query.Where("address ILIKE ?", "%"+f.Address+"%")
}
// Manual preload untuk roles dengan field terbatas
// for _, include := range f.Includes {
// if include == "Roles" {
// query = query.Preload("Roles", func(db *gorm.DB) *gorm.DB {
// return db.Select("id", "name") // Hanya ambil id dan name
// })
// }
// }
return query
}
func (f *ClientFilter) GetTableName() string {
return "m_clients"
}
func (f *ClientFilter) GetSearchFields() []string {
return []string{"name"}
}
func (f *ClientFilter) GetDefaultSort() string {
return "id asc"
}
func (f *ClientFilter) GetIncludes() []string {
return f.Includes
}
func (f *ClientFilter) GetPagination() pagination.PaginationRequest {
return f.Pagination
}
func (f *ClientFilter) Validate() {
var validIncludes []string
allowedIncludes := f.GetAllowedIncludes()
for _, include := range f.Includes {
if allowedIncludes[include] {
validIncludes = append(validIncludes, include)
}
}
f.Includes = validIncludes
}
func (f *ClientFilter) GetAllowedIncludes() map[string]bool {
return map[string]bool{
// "Roles": true,
}
}

View File

@ -0,0 +1,82 @@
package repository
import (
"context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type (
ClientRepository interface {
Create(ctx context.Context, tx *gorm.DB, client entities.M_Client) (entities.M_Client, error)
GetById(ctx context.Context, tx *gorm.DB, clientId string) (entities.M_Client, error)
GetByName(ctx context.Context, tx *gorm.DB, name string) (entities.M_Client, error)
Update(ctx context.Context, tx *gorm.DB, client entities.M_Client) (entities.M_Client, error)
Delete(ctx context.Context, tx *gorm.DB, clientId string) error
}
clientRepository struct {
db *gorm.DB
}
)
func (r *clientRepository) Create(ctx context.Context, tx *gorm.DB, client entities.M_Client) (entities.M_Client, error) {
if tx == nil {
tx = r.db
}
if client.ID == uuid.Nil {
client.ID = uuid.New()
}
if err := tx.WithContext(ctx).Create(&client).Error; err != nil {
return entities.M_Client{}, err
}
return client, nil
}
func (r *clientRepository) GetById(ctx context.Context, tx *gorm.DB, clientId string) (entities.M_Client, error) {
if tx == nil {
tx = r.db
}
var client entities.M_Client
if err := tx.WithContext(ctx).First(&client, "id = ?", clientId).Error; err != nil {
return entities.M_Client{}, err
}
return client, nil
}
func (r *clientRepository) GetByName(ctx context.Context, tx *gorm.DB, name string) (entities.M_Client, error) {
if tx == nil {
tx = r.db
}
var client entities.M_Client
if err := tx.WithContext(ctx).Where("name = ?", name).First(&client).Error; err != nil {
return entities.M_Client{}, err
}
return client, nil
}
func (r *clientRepository) Update(ctx context.Context, tx *gorm.DB, client entities.M_Client) (entities.M_Client, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Updates(&client).Error; err != nil {
return entities.M_Client{}, err
}
return client, nil
}
func (r *clientRepository) Delete(ctx context.Context, tx *gorm.DB, clientId string) error {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Delete(&entities.M_Client{}, "id = ?", clientId).Error; err != nil {
return err
}
return nil
}
func NewClientRepository(db *gorm.DB) ClientRepository {
return &clientRepository{db: db}
}

24
modules/client/routes.go Normal file
View File

@ -0,0 +1,24 @@
package client
import (
"github.com/Caknoooo/go-gin-clean-starter/middlewares"
"github.com/Caknoooo/go-gin-clean-starter/modules/auth/service"
"github.com/Caknoooo/go-gin-clean-starter/modules/client/controller"
"github.com/Caknoooo/go-gin-clean-starter/pkg/constants"
"github.com/gin-gonic/gin"
"github.com/samber/do"
)
func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
clientController := do.MustInvoke[controller.ClientController](injector)
jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService)
clientRoutes := server.Group("/api/v1/clients")
{
clientRoutes.POST("", middlewares.Authenticate(jwtService), clientController.Create)
clientRoutes.GET("/:id", middlewares.Authenticate(jwtService), clientController.GetById)
clientRoutes.PUT("/:id", middlewares.Authenticate(jwtService), clientController.Update)
clientRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), clientController.Delete)
clientRoutes.GET("", middlewares.Authenticate(jwtService), clientController.GetAll)
}
}

View File

@ -0,0 +1,190 @@
package service
import (
"context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/Caknoooo/go-gin-clean-starter/modules/client/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/client/repository"
"gorm.io/gorm"
)
type ClientService interface {
Create(ctx context.Context, client dto.ClientCreateRequest) (dto.ClientResponse, error)
GetById(ctx context.Context, clientId string) (dto.ClientResponse, error)
GetByName(ctx context.Context, name string) (dto.ClientResponse, error)
Update(ctx context.Context, client dto.ClientUpdateRequest, clientId string) (dto.ClientResponse, error)
Delete(ctx context.Context, clientId string) error
}
type clientService struct {
db *gorm.DB
clientRepo repository.ClientRepository
}
func (s *clientService) Create(ctx context.Context, req dto.ClientCreateRequest) (dto.ClientResponse, error) {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
client := entities.M_Client{
Name: req.Name,
PIC: req.PIC,
Phone: req.Phone,
Email: req.Email,
Address: req.Address,
Logo: req.Logo, // []byte, sudah benar untuk simpan file
}
createdClient, err := s.clientRepo.Create(ctx, tx, client)
if err != nil {
tx.Rollback()
return dto.ClientResponse{}, err
}
err = tx.Commit().Error
if err != nil {
tx.Rollback()
return dto.ClientResponse{}, err
}
// Mapping entity ke response (Logo bisa dikonversi ke base64 jika perlu)
// logoBase64 := ""
// if len(createdClient.Logo) > 0 {
// logoBase64 = base64.StdEncoding.EncodeToString(createdClient.Logo)
// }
return dto.ClientResponse{
ID: createdClient.ID.String(),
Name: createdClient.Name,
PIC: createdClient.PIC,
Phone: createdClient.Phone,
Email: createdClient.Email,
Address: createdClient.Address,
Logo: req.Logo,
}, nil
}
func (s *clientService) GetById(ctx context.Context, clientId string) (dto.ClientResponse, error) {
client, err := s.clientRepo.GetById(ctx, nil, clientId)
if err != nil {
return dto.ClientResponse{}, err
}
// logoBase64 := ""
// if len(client.Logo) > 0 {
// logoBase64 = base64.StdEncoding.EncodeToString(client.Logo)
// }
return dto.ClientResponse{
ID: client.ID.String(),
Name: client.Name,
PIC: client.PIC,
Phone: client.Phone,
Email: client.Email,
Address: client.Address,
Logo: client.Logo,
}, nil
}
func (s *clientService) GetByName(ctx context.Context, name string) (dto.ClientResponse, error) {
client, err := s.clientRepo.GetByName(ctx, nil, name)
if err != nil {
return dto.ClientResponse{}, err
}
// logoBase64 := ""
// if len(client.Logo) > 0 {
// logoBase64 = base64.StdEncoding.EncodeToString(client.Logo)
// }
return dto.ClientResponse{
ID: client.ID.String(),
Name: client.Name,
PIC: client.PIC,
Phone: client.Phone,
Email: client.Email,
Address: client.Address,
Logo: client.Logo,
}, nil
}
func (s *clientService) Update(ctx context.Context, req dto.ClientUpdateRequest, clientId string) (dto.ClientResponse, error) {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
client, err := s.clientRepo.GetById(ctx, tx, clientId)
if err != nil {
tx.Rollback()
return dto.ClientResponse{}, err
}
if req.Name != nil {
client.Name = *req.Name
}
if req.PIC != nil {
client.PIC = *req.PIC
}
if req.Phone != nil {
client.Phone = *req.Phone
}
if req.Email != nil {
client.Email = *req.Email
}
if req.Address != nil {
client.Address = *req.Address
}
if req.Logo != nil {
client.Logo = *req.Logo
}
updatedClient, err := s.clientRepo.Update(ctx, tx, client)
if err != nil {
tx.Rollback()
return dto.ClientResponse{}, err
}
err = tx.Commit().Error
if err != nil {
tx.Rollback()
return dto.ClientResponse{}, err
}
// logoBase64 := ""
// if len(updatedClient.Logo) > 0 {
// logoBase64 = base64.StdEncoding.EncodeToString(updatedClient.Logo)
// }
return dto.ClientResponse{
ID: updatedClient.ID.String(),
Name: updatedClient.Name,
PIC: updatedClient.PIC,
Phone: updatedClient.Phone,
Email: updatedClient.Email,
Address: updatedClient.Address,
Logo: updatedClient.Logo,
}, nil
}
func (s *clientService) Delete(ctx context.Context, clientId string) error {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
err := s.clientRepo.Delete(ctx, tx, clientId)
if err != nil {
tx.Rollback()
return err
}
err = tx.Commit().Error
if err != nil {
tx.Rollback()
return err
}
return nil
}
func NewClientService(clientRepo repository.ClientRepository, db *gorm.DB) ClientService {
return &clientService{
clientRepo: clientRepo,
db: db,
}
}

View File

@ -4,6 +4,9 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/config"
authRepo "github.com/Caknoooo/go-gin-clean-starter/modules/auth/repository"
"github.com/Caknoooo/go-gin-clean-starter/modules/auth/service"
clientController "github.com/Caknoooo/go-gin-clean-starter/modules/client/controller"
clientRepo "github.com/Caknoooo/go-gin-clean-starter/modules/client/repository"
clientService "github.com/Caknoooo/go-gin-clean-starter/modules/client/service"
"github.com/sirupsen/logrus"
// productController "github.com/Caknoooo/go-gin-clean-starter/modules/product/controller"
@ -58,8 +61,9 @@ func RegisterDependencies(injector *do.Injector) {
userRepository := repository.NewUserRepository(db)
refreshTokenRepository := authRepo.NewRefreshTokenRepository(db)
// productRepository := productRepo.NewProductRepository(db)
roleRepository := roleRepo.NewRoleRepository(db)
menuRepository := menuRepo.NewMenuRepository(db)
roleRepository := roleRepo.NewRoleRepository(db)
clientRepository := clientRepo.NewClientRepository(db)
maintenanceGroupRepository := maintGroupRepo.NewMaintGroupRepository(db)
maintenanceGroupRoleRepository := maintGroupRepoRole.NewMaintGroupRoleRepository(db)
maintenanceGroupRoleUserRepository := maintGroupRepoRoleUser.NewMaintGroupRoleUserRepository(db)
@ -70,8 +74,14 @@ func RegisterDependencies(injector *do.Injector) {
roleService := roleService.NewRoleService(roleRepository, refreshTokenRepository, jwtService, userServ, db)
menuSvc := menuService.NewMenuService(menuRepository, jwtService, db)
maintenanceGroupServ := maintGroupService.NewMaintenanceGroupService(maintenanceGroupRepository, maintenanceGroupRoleRepository, maintenanceGroupRoleUserRepository, db)
clientServ := clientService.NewClientService(clientRepository, db)
// Controller
do.Provide(
injector, func(i *do.Injector) (clientController.ClientController, error) {
return clientController.NewClientController(i, clientServ), nil
},
)
do.Provide(
injector, func(i *do.Injector) (controller.UserController, error) {
return controller.NewUserController(i, userServ), nil