Add User Partner and Product

This commit is contained in:
aditya.siregar 2024-06-03 14:40:50 +07:00
parent 67f1dbc850
commit 8a23e72230
46 changed files with 1366 additions and 213 deletions

View File

@ -30,6 +30,3 @@ deploy_to_staging:
- kubectl apply -f k8s/staging/ingress.yaml - kubectl apply -f k8s/staging/ingress.yaml
only: only:
- main - main
# tes bintang 4

View File

@ -1,9 +1,9 @@
PROJECT_NAME = "furtuna-backend" PROJECT_NAME = "furtuna-backend"
DB_USERNAME := furtuna_admin DB_USERNAME := fortuna_admin
DB_PASSWORD := Z4G827t9428QFQ%5ESZXW%2343dB%25%214Bmh80 DB_PASSWORD := Z4G827t9428QFQ%5ESZXW%2343dB%25%214Bmh80
DB_HOST := 103.96.146.124 DB_HOST := 103.96.146.124
DB_PORT := 1960 DB_PORT := 1960
DB_NAME := furtuna-staging DB_NAME := fortuna-staging
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable

1
furtuna-frontend Submodule

@ -0,0 +1 @@
Subproject commit 19af799abdd523522fad579044d55f5d764f87a3

View File

@ -72,6 +72,9 @@ func (s *ServiceException) MapErrorsToHTTPCode() int {
case errInvalidLogin: case errInvalidLogin:
return http.StatusBadRequest return http.StatusBadRequest
case errUserIsNotFound:
return http.StatusBadRequest
default: default:
return http.StatusInternalServerError return http.StatusInternalServerError
} }
@ -89,6 +92,9 @@ func (s *ServiceException) MapErrorsToCode() Code {
case errBadRequest: case errBadRequest:
return BadRequest return BadRequest
case errUserIsNotFound:
return BadRequest
default: default:
return ServerError return ServerError
} }

View File

@ -13,6 +13,7 @@ type Context interface {
RequestedBy() int64 RequestedBy() int64
IsSuperAdmin() bool IsSuperAdmin() bool
GetPartnerID() *int64
} }
type MyContextImpl struct { type MyContextImpl struct {
@ -20,7 +21,7 @@ type MyContextImpl struct {
requestedBy int64 requestedBy int64
requestID string requestID string
branchID int64 partnerID int64
roleID int roleID int
} }
@ -32,11 +33,18 @@ func (m *MyContextImpl) IsSuperAdmin() bool {
return m.roleID == int(role.SuperAdmin) return m.roleID == int(role.SuperAdmin)
} }
func (m *MyContextImpl) GetPartnerID() *int64 {
if m.partnerID != 0 {
return &m.partnerID
}
return nil
}
func NewMyContext(parent context.Context, claims *entity.JWTAuthClaims) (*MyContextImpl, error) { func NewMyContext(parent context.Context, claims *entity.JWTAuthClaims) (*MyContextImpl, error) {
return &MyContextImpl{ return &MyContextImpl{
Context: parent, Context: parent,
requestedBy: claims.UserID, requestedBy: claims.UserID,
branchID: claims.BranchID, partnerID: claims.PartnerID,
roleID: claims.Role, roleID: claims.Role,
}, nil }, nil
} }

View File

@ -4,6 +4,8 @@ type Role int64
const ( const (
SuperAdmin Role = 1 SuperAdmin Role = 1
BranchAdmin Role = 2 Admin Role = 2
CasheerAdmin Role = 3 PartnerAdmin Role = 3
SiteAdmin Role = 4
Casheer Role = 5
) )

View File

@ -3,8 +3,8 @@ package studio
type StudioStatus string type StudioStatus string
const ( const (
Active StudioStatus = "Active" Active StudioStatus = "active"
Inactive StudioStatus = "Inactive" Inactive StudioStatus = "inactive"
) )
func (b StudioStatus) toString() string { func (b StudioStatus) toString() string {

View File

@ -14,23 +14,24 @@ type AuthData struct {
} }
type UserDB struct { type UserDB struct {
ID int64 `gorm:"primary_key;column:id" json:"id"` ID int64 `gorm:"primary_key;column:id" json:"id"`
Name string `gorm:"column:name" json:"name"` Name string `gorm:"column:name" json:"name"`
Email string `gorm:"column:email" json:"email"` Email string `gorm:"column:email" json:"email"`
Password string `gorm:"column:password" json:"-"` Password string `gorm:"column:password" json:"-"`
Status userstatus.UserStatus `gorm:"column:status" json:"status"` Status userstatus.UserStatus `gorm:"column:status" json:"status"`
UserType string `gorm:"column:user_type" json:"user_type"` UserType string `gorm:"column:user_type" json:"user_type"`
PhoneNumber string `gorm:"column:phone_number" json:"phone_number"` PhoneNumber string `gorm:"column:phone_number" json:"phone_number"`
NIK string `gorm:"column:nik" json:"nik"` NIK string `gorm:"column:nik" json:"nik"`
RoleID int64 `gorm:"column:role_id" json:"role_id"` RoleID int64 `gorm:"column:role_id" json:"role_id"`
RoleName string `gorm:"column:role_name" json:"role_name"` RoleName string `gorm:"column:role_name" json:"role_name"`
BranchID *int64 `gorm:"column:partner_id" json:"partner_id"` PartnerID *int64 `gorm:"column:partner_id" json:"partner_id"`
BranchName string `gorm:"column:partner_name" json:"partner_name"` PartnerName string `gorm:"column:partner_name" json:"partner_name"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` PartnerStatus string `gorm:"column:partner_status" json:"partner_status"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
DeletedAt *time.Time `gorm:"column:deleted_at" json:"deleted_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
CreatedBy int64 `gorm:"column:created_by" json:"created_by"` DeletedAt *time.Time `gorm:"column:deleted_at" json:"deleted_at"`
UpdatedBy int64 `gorm:"column:updated_by" json:"updated_by"` CreatedBy int64 `gorm:"column:created_by" json:"created_by"`
UpdatedBy int64 `gorm:"column:updated_by" json:"updated_by"`
} }
func (u *UserDB) ToUser() *User { func (u *UserDB) ToUser() *User {
@ -39,16 +40,16 @@ func (u *UserDB) ToUser() *User {
} }
userEntity := &User{ userEntity := &User{
ID: u.ID, ID: u.ID,
Name: u.Name, Name: u.Name,
Email: u.Email, Email: u.Email,
Status: u.Status, Status: u.Status,
CreatedAt: u.CreatedAt, CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt, UpdatedAt: u.UpdatedAt,
RoleID: role.Role(u.RoleID), RoleID: role.Role(u.RoleID),
RoleName: u.RoleName, RoleName: u.RoleName,
PartnerID: u.BranchID, PartnerID: u.PartnerID,
BranchName: u.BranchName, PartnerName: u.PartnerName,
} }
return userEntity return userEntity
@ -63,7 +64,7 @@ func (u *UserDB) ToUserRoleDB() *UserRoleDB {
ID: 0, ID: 0,
UserID: u.ID, UserID: u.ID,
RoleID: u.RoleID, RoleID: u.RoleID,
PartnerID: u.BranchID, PartnerID: u.PartnerID,
CreatedAt: u.CreatedAt, CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt, UpdatedAt: u.UpdatedAt,
} }
@ -77,12 +78,13 @@ func (UserDB) TableName() string {
func (u *UserDB) ToUserAuthenticate(signedToken string) *AuthenticateUser { func (u *UserDB) ToUserAuthenticate(signedToken string) *AuthenticateUser {
return &AuthenticateUser{ return &AuthenticateUser{
Token: signedToken, Token: signedToken,
Name: u.Name, Name: u.Name,
RoleID: role.Role(u.RoleID), RoleID: role.Role(u.RoleID),
RoleName: u.RoleName, RoleName: u.RoleName,
BranchID: u.BranchID, PartnerID: u.PartnerID,
BranchName: u.BranchName, PartnerName: u.PartnerName,
PartnerStatus: u.PartnerStatus,
} }
} }
@ -116,7 +118,7 @@ func (u *UserDB) ToUpdatedUser(req User) error {
} }
if *req.PartnerID > 0 { if *req.PartnerID > 0 {
u.BranchID = req.PartnerID u.PartnerID = req.PartnerID
} }
if req.RoleID > 0 { if req.RoleID > 0 {

View File

@ -3,10 +3,10 @@ package entity
import "github.com/golang-jwt/jwt" import "github.com/golang-jwt/jwt"
type JWTAuthClaims struct { type JWTAuthClaims struct {
UserID int64 `json:"id"` UserID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Role int `json:"role"` Role int `json:"role"`
BranchID int64 `json:"branch_id"` PartnerID int64 `json:"partner_id"`
jwt.StandardClaims jwt.StandardClaims
} }

View File

@ -1,19 +1,42 @@
package entity package entity
import ( import (
"furtuna-be/internal/constants/role"
"time" "time"
) )
type CreatePartnerRequest struct {
Name string `json:"name" validate:"required"`
Address string `json:"address"`
Username string `json:"username" validate:"required"`
FullName string `json:"full_name"`
Email string `json:"email"`
Password string `json:"password" validate:"required"`
NIK string `json:"nik"`
PhoneNumber string `json:"phone_number"`
BankName string `json:"bank_name"`
BankAccountNumber string `json:"bank_account_number"`
BankAccountHolderName string `json:"bank_account_holder_name"`
}
type Partner struct { type Partner struct {
ID int64 ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
Name string Name string `gorm:"type:varchar(255);not null;column:name"`
Status string Status string `gorm:"type:varchar(50);column:status"`
Address string LicenseExpiredDate *time.Time `gorm:"type:date;column:license_expired_date"`
CreatedAt time.Time Address string `gorm:"type:varchar(255);column:address"`
UpdatedAt time.Time BankName string `gorm:"type:varchar(255);column:bank_name"`
DeletedAt *time.Time BankAccountNumber string `gorm:"type:varchar(50);column:bank_account_number"`
CreatedBy int64 BankAccountHolderName string `gorm:"type:varchar(255);column:bank_account_holder_name"`
UpdatedBy int64 CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
DeletedAt *time.Time `gorm:"column:deleted_at"`
CreatedBy int64 `gorm:"type:int;column:created_by"`
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
}
func (Partner) TableName() string {
return "partners"
} }
type PartnerSearch struct { type PartnerSearch struct {
@ -80,3 +103,46 @@ func (o *PartnerDB) SetDeleted(updatedBy int64) {
o.DeletedAt = &currentTime o.DeletedAt = &currentTime
o.UpdatedBy = updatedBy o.UpdatedBy = updatedBy
} }
func (c *CreatePartnerRequest) ToUserAdmin(partnerID int64) *User {
return &User{
Name: c.FullName,
Password: c.Password,
Email: c.Email,
NIK: c.NIK,
Status: "active",
RoleID: role.PartnerAdmin,
PartnerID: &partnerID,
}
}
func (e *CreatePartnerRequest) ToPartnerDB(createdBy int64) *PartnerDB {
twoDays := 48 * time.Hour
licenseExpiredDate := time.Now().Add(twoDays)
return &PartnerDB{
Partner: Partner{
Name: e.Name,
Status: "inactive",
Address: e.Address,
CreatedBy: createdBy,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
BankAccountHolderName: e.BankAccountHolderName,
BankAccountNumber: e.BankAccountNumber,
BankName: e.BankName,
LicenseExpiredDate: &licenseExpiredDate,
},
}
}
func (e *CreatePartnerRequest) ToWallet(partnerID int64) *Wallet {
return &Wallet{
PartnerID: partnerID,
Balance: 0,
Currency: "IDR",
Status: "active",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}

View File

@ -6,20 +6,25 @@ import (
) )
type Product struct { type Product struct {
ID int64 ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
Name string PartnerID int64 `gorm:"type:int;column:partner_id"`
Type product.ProductType SiteID int64 `gorm:"type:int;column:site_id"`
Price float64 Name string `gorm:"type:varchar(255);not null;column:name"`
Status product.ProductStatus Type string `gorm:"type:varchar;column:type"`
Description string Price float64 `gorm:"type:decimal;column:price"`
Image string IsWeekendTicket bool `gorm:"type:bool;column:is_weekend_ticket"`
BranchID int64 IsSeasonTicket bool `gorm:"type:bool;column:is_season_ticket"`
StockQty int64 Status string `gorm:"type:varchar;column:status"`
CreatedAt time.Time Description string `gorm:"type:varchar(255);not null;column:description"`
UpdatedAt time.Time CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
DeletedAt *time.Time UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
CreatedBy int64 DeletedAt *time.Time `gorm:"column:deleted_at"`
UpdatedBy int64 CreatedBy int64 `gorm:"type:int;column:created_by"`
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
}
func (Product) TableName() string {
return "products"
} }
type ProductSearch struct { type ProductSearch struct {
@ -56,9 +61,7 @@ func (e *ProductDB) ToProduct() *Product {
Price: e.Price, Price: e.Price,
Status: e.Status, Status: e.Status,
Description: e.Description, Description: e.Description,
Image: e.Image, PartnerID: e.PartnerID,
BranchID: e.BranchID,
StockQty: e.StockQty,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
DeletedAt: e.DeletedAt, DeletedAt: e.DeletedAt,
@ -97,14 +100,6 @@ func (o *ProductDB) ToUpdatedProduct(updatedby int64, req Product) {
if req.Description != "" { if req.Description != "" {
o.Description = req.Description o.Description = req.Description
} }
if req.Image != "" {
o.Image = req.Image
}
if req.StockQty > 0 {
o.StockQty = req.StockQty
}
} }
func (o *ProductDB) SetDeleted(updatedby int64) { func (o *ProductDB) SetDeleted(updatedby int64) {

145
internal/entity/sites.go Normal file
View File

@ -0,0 +1,145 @@
package entity
import (
"time"
)
type Site struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
Name string `gorm:"type:varchar(255);not null;column:name"`
PartnerID int64 `gorm:"type:int;column:partner_id"`
Image string `gorm:"type:varchar;column:image"`
Address string `gorm:"type:varchar;column:address"`
LocationLink string `gorm:"type:varchar;column:location_link"`
Description string `gorm:"type:varchar;column:description"`
Highlight string `gorm:"type:varchar;column:highlight"`
ContactPerson string `gorm:"type:varchar;column:contact_person"`
TnC string `gorm:"type:varchar;column:tnc"`
AdditionalInfo string `gorm:"type:varchar;column:additional_info"`
Status string `gorm:"type:varchar;column:status"`
IsSeasonTicket bool `gorm:"type:bool;column:is_season_ticket"`
IsDiscountActive bool `gorm:"type:bool;column:is_discount_active"`
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
DeletedAt *time.Time `gorm:"column:deleted_at"`
CreatedBy int64 `gorm:"type:int;column:created_by"`
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
Products []Product `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"`
}
type SiteSearch struct {
Search string
Name string
Limit int
Offset int
}
type SiteList []*SiteDB
type SiteDB struct {
Site
}
func (s *Site) ToSiteDB() *SiteDB {
return &SiteDB{
Site: *s,
}
}
func (SiteDB) TableName() string {
return "sites"
}
func (e *SiteDB) ToSite() *Site {
return &Site{
ID: e.ID,
Name: e.Name,
PartnerID: e.PartnerID,
Image: e.Image,
Address: e.Address,
LocationLink: e.LocationLink,
Description: e.Description,
Highlight: e.Highlight,
ContactPerson: e.ContactPerson,
TnC: e.TnC,
AdditionalInfo: e.AdditionalInfo,
Status: e.Status,
IsSeasonTicket: e.IsSeasonTicket,
IsDiscountActive: e.IsDiscountActive,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
DeletedAt: e.DeletedAt,
CreatedBy: e.CreatedBy,
UpdatedBy: e.UpdatedBy,
}
}
func (s *SiteList) ToSiteList() []*Site {
var sites []*Site
for _, site := range *s {
sites = append(sites, site.ToSite())
}
return sites
}
func (o *SiteDB) ToUpdatedSite(updatedBy int64, req Site) {
o.UpdatedBy = updatedBy
if req.Name != "" {
o.Name = req.Name
}
if req.PartnerID != 0 {
o.PartnerID = req.PartnerID
}
if req.Image != "" {
o.Image = req.Image
}
if req.Address != "" {
o.Address = req.Address
}
if req.LocationLink != "" {
o.LocationLink = req.LocationLink
}
if req.Description != "" {
o.Description = req.Description
}
if req.Highlight != "" {
o.Highlight = req.Highlight
}
if req.ContactPerson != "" {
o.ContactPerson = req.ContactPerson
}
if req.TnC != "" {
o.TnC = req.TnC
}
if req.AdditionalInfo != "" {
o.AdditionalInfo = req.AdditionalInfo
}
if req.Status != "" {
o.Status = req.Status
}
if req.IsSeasonTicket {
o.IsSeasonTicket = req.IsSeasonTicket
}
if req.IsDiscountActive {
o.IsDiscountActive = req.IsDiscountActive
}
}
func (o *SiteDB) SetDeleted(updatedBy int64) {
currentTime := time.Now()
o.DeletedAt = &currentTime
o.UpdatedBy = updatedBy
}

View File

@ -10,27 +10,28 @@ import (
) )
type User struct { type User struct {
ID int64 ID int64
Name string Name string
Email string Email string
Password string Password string
Status userstatus.UserStatus Status userstatus.UserStatus
NIK string NIK string
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
RoleID role.Role RoleID role.Role
RoleName string RoleName string
PartnerID *int64 PartnerID *int64
BranchName string PartnerName string
} }
type AuthenticateUser struct { type AuthenticateUser struct {
Token string Token string
Name string Name string
RoleID role.Role RoleID role.Role
RoleName string RoleName string
BranchID *int64 PartnerID *int64
BranchName string PartnerName string
PartnerStatus string
} }
type UserRoleDB struct { type UserRoleDB struct {
@ -53,7 +54,7 @@ func (u *User) ToUserDB(createdBy int64) (*UserDB, error) {
return nil, err return nil, err
} }
if u.RoleID == role.BranchAdmin && u.PartnerID == nil { if u.RoleID == role.Admin && u.PartnerID == nil {
return nil, errors.New("invalid request") return nil, errors.New("invalid request")
} }
@ -62,7 +63,7 @@ func (u *User) ToUserDB(createdBy int64) (*UserDB, error) {
Email: u.Email, Email: u.Email,
Password: hashedPassword, Password: hashedPassword,
RoleID: int64(u.RoleID), RoleID: int64(u.RoleID),
BranchID: u.PartnerID, PartnerID: u.PartnerID,
Status: userstatus.Active, Status: userstatus.Active,
CreatedBy: createdBy, CreatedBy: createdBy,
}, nil }, nil

17
internal/entity/wallet.go Normal file
View File

@ -0,0 +1,17 @@
package entity
import "time"
type Wallet struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
PartnerID int64 `gorm:"type:int;not null;column:partner_id"`
Balance float64 `gorm:"type:decimal(18,2);not null;default:0.00;column:balance"`
Currency string `gorm:"type:varchar(3);not null;column:currency"`
Status string `gorm:"type:varchar(50);column:status"`
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
}
func (Wallet) TableName() string {
return "wallets"
}

View File

@ -51,19 +51,20 @@ func (h *AuthHandler) AuthLogin(c *gin.Context) {
return return
} }
var branch *response.Branch var partner *response.Partner
if authUser.RoleID != role.SuperAdmin { if authUser.RoleID != role.SuperAdmin {
branch = &response.Branch{ partner = &response.Partner{
ID: authUser.BranchID, ID: authUser.PartnerID,
Name: authUser.BranchName, Name: authUser.PartnerName,
Status: authUser.PartnerStatus,
} }
} }
resp := response.LoginResponse{ resp := response.LoginResponse{
Token: authUser.Token, Token: authUser.Token,
Branch: branch, Partner: partner,
Name: authUser.Name, Name: authUser.Name,
Role: response.Role{ Role: response.Role{
ID: int64(authUser.RoleID), ID: int64(authUser.RoleID),
Role: authUser.RoleName, Role: authUser.RoleName,

View File

@ -51,7 +51,7 @@ func NewHandler(service services.Partner) *Handler {
func (h *Handler) Create(c *gin.Context) { func (h *Handler) Create(c *gin.Context) {
ctx := request.GetMyContext(c) ctx := request.GetMyContext(c)
var req request.Partner var req request.CreatePartnerRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest) response.ErrorWrapper(c, errors.ErrorBadRequest)
return return

View File

@ -246,9 +246,6 @@ func (h *Handler) toProductResponse(resp *entity.Product) response.Product {
Price: resp.Price, Price: resp.Price,
Status: resp.Status, Status: resp.Status,
Description: resp.Description, Description: resp.Description,
Image: resp.Image,
BranchID: resp.BranchID,
StockQty: resp.StockQty,
CreatedAt: resp.CreatedAt.Format(time.RFC3339), CreatedAt: resp.CreatedAt.Format(time.RFC3339),
UpdatedAt: resp.CreatedAt.Format(time.RFC3339), UpdatedAt: resp.CreatedAt.Format(time.RFC3339),
} }

View File

@ -0,0 +1,299 @@
package site
import (
"furtuna-be/internal/common/errors"
"furtuna-be/internal/entity"
"furtuna-be/internal/handlers/request"
"furtuna-be/internal/handlers/response"
"furtuna-be/internal/services"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type Handler struct {
service services.Site
}
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route := group.Group("/site")
route.POST("/", jwt, h.Create)
route.GET("/list", jwt, h.GetAll)
route.PUT("/:id", jwt, h.Update)
route.GET("/:id", jwt, h.GetByID)
route.DELETE("/:id", jwt, h.Delete)
}
func NewHandler(service services.Site) *Handler {
return &Handler{
service: service,
}
}
// Create handles the creation of a new Site.
// @Summary Create a new Site
// @Description Create a new Site based on the provided data.
// @Accept json
// @Produce json
// @Param Authorization header string true "JWT token"
// @Param req body request.Site true "New Site details"
// @Success 200 {object} response.BaseResponse{data=response.Site} "Site created successfully"
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
// @Router /api/v1/site [post]
// @Tags Site APIs
func (h *Handler) Create(c *gin.Context) {
ctx := request.GetMyContext(c)
var req request.Site
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
if !ctx.IsSuperAdmin() {
req.PartnerID = ctx.GetPartnerID()
}
validate := validator.New()
if err := validate.Struct(req); err != nil {
response.ErrorWrapper(c, err)
return
}
res, err := h.service.Create(ctx, req.ToEntity(ctx.RequestedBy()))
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: h.toSiteResponse(res),
})
}
// Update handles the update of an existing Site.
// @Summary Update an existing Site
// @Description Update the details of an existing Site based on the provided ID.
// @Accept json
// @Produce json
// @Param Authorization header string true "JWT token"
// @Param id path int64 true "Site ID to update"
// @Param req body request.Site true "Updated Site details"
// @Success 200 {object} response.BaseResponse{data=response.Site} "Site updated successfully"
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
// @Router /api/v1/site/{id} [put]
// @Tags Site APIs
func (h *Handler) Update(c *gin.Context) {
ctx := request.GetMyContext(c)
id := c.Param("id")
SiteID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
var req request.Site
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
validate := validator.New()
if err := validate.Struct(req); err != nil {
response.ErrorWrapper(c, err)
return
}
updatedSite, err := h.service.Update(ctx, SiteID, req.ToEntity(ctx.RequestedBy()))
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: h.toSiteResponse(updatedSite),
})
}
// GetAll retrieves a list of Sites.
// @Summary Get a list of Sites
// @Description Get a paginated list of Sites based on query parameters.
// @Accept json
// @Produce json
// @Param Authorization header string true "JWT token"
// @Param Limit query int false "Number of items to retrieve (default 10)"
// @Param Offset query int false "Offset for pagination (default 0)"
// @Success 200 {object} response.BaseResponse{data=response.SiteList} "List of Sites"
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
// @Router /api/v1/site/list [get]
// @Tags Site APIs
func (h *Handler) GetAll(c *gin.Context) {
var req request.SiteParam
if err := c.ShouldBindQuery(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
Sites, total, err := h.service.GetAll(c.Request.Context(), req.ToEntity())
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: h.toSiteResponseList(Sites, int64(total), req),
})
}
// Delete handles the deletion of a Site by ID.
// @Summary Delete a Site by ID
// @Description Delete a Site based on the provided ID.
// @Accept json
// @Produce json
// @Param Authorization header string true "JWT token"
// @Param id path int64 true "Site ID to delete"
// @Success 200 {object} response.BaseResponse "Site deleted successfully"
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
// @Router /api/v1/site/{id} [delete]
// @Tags Site APIs
func (h *Handler) Delete(c *gin.Context) {
ctx := request.GetMyContext(c)
id := c.Param("id")
// Parse the ID into a uint
SiteID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
err = h.service.Delete(ctx, SiteID)
if err != nil {
c.JSON(http.StatusInternalServerError, response.BaseResponse{
Success: false,
Status: http.StatusInternalServerError,
Message: err.Error(),
Data: nil,
})
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: nil,
})
}
// GetByID retrieves details of a specific Site by ID.
// @Summary Get details of a Site by ID
// @Description Get details of a Site based on the provided ID.
// @Accept json
// @Produce json
// @Param Authorization header string true "JWT token"
// @Param id path int64 true "Site ID to retrieve"
// @Success 200 {object} response.BaseResponse{data=response.Site} "Site details"
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
// @Router /api/v1/site/{id} [get]
// @Tags Site APIs
func (h *Handler) GetByID(c *gin.Context) {
id := c.Param("id")
// Parse the ID into a uint
SiteID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
res, err := h.service.GetByID(c.Request.Context(), SiteID)
if err != nil {
c.JSON(http.StatusInternalServerError, response.BaseResponse{
Success: false,
Status: http.StatusInternalServerError,
Message: err.Error(),
Data: nil,
})
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: h.toSiteResponse(res),
})
}
func (h *Handler) toSiteResponse(resp *entity.Site) response.Site {
return response.Site{
ID: &resp.ID,
Name: resp.Name,
PartnerID: resp.PartnerID,
Image: resp.Image,
Address: resp.Address,
LocationLink: resp.LocationLink,
Description: resp.Description,
Highlight: resp.Highlight,
ContactPerson: resp.ContactPerson,
TnC: resp.TnC,
AdditionalInfo: resp.AdditionalInfo,
Status: resp.Status,
IsSeasonTicket: resp.IsSeasonTicket,
IsDiscountActive: resp.IsDiscountActive,
CreatedAt: resp.CreatedAt.Format(time.RFC3339),
UpdatedAt: resp.UpdatedAt.Format(time.RFC3339),
}
}
func (h *Handler) toSiteResponseList(resp []*entity.Site, total int64, req request.SiteParam) response.SiteList {
var Sites []response.Site
for _, b := range resp {
Sites = append(Sites, h.toSiteResponse(b))
}
return response.SiteList{
Sites: Sites,
Total: total,
Limit: req.Limit,
Offset: req.Offset,
}
}
func (h *Handler) toProductResponseList(products []entity.Product) []response.Product {
var res []response.Product
for _, product := range products {
res = append(res, response.Product{
ID: product.ID,
PartnerID: product.PartnerID,
SiteID: product.SiteID,
Name: product.Name,
Type: product.Type,
Price: product.Price,
IsWeekendTicket: product.IsWeekendTicket,
IsSeasonTicket: product.IsSeasonTicket,
Status: product.Status,
Description: product.Description,
CreatedAt: product.CreatedAt.Format(time.RFC3339),
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
})
}
return res
}

View File

@ -56,13 +56,15 @@ func (h *Handler) Create(c *gin.Context) {
return return
} }
req.PartnerID = ctx.GetPartnerID()
if err := req.Validate(); err != nil { if err := req.Validate(); err != nil {
response.ErrorWrapper(c, errors.ErrorInvalidRequest) response.ErrorWrapper(c, errors.ErrorInvalidRequest)
return return
} }
res, err := h.service.Create(ctx, req.ToEntity()) ctx.IsSuperAdmin()
res, err := h.service.Create(ctx, req.ToEntity())
if err != nil { if err != nil {
response.ErrorWrapper(c, err) response.ErrorWrapper(c, err)
return return
@ -260,16 +262,16 @@ func (h *Handler) Delete(c *gin.Context) {
func (h *Handler) toUserResponse(resp *entity.User) response.User { func (h *Handler) toUserResponse(resp *entity.User) response.User {
return response.User{ return response.User{
ID: resp.ID, ID: resp.ID,
Name: resp.Name, Name: resp.Name,
Email: resp.Email, Email: resp.Email,
Status: string(resp.Status), Status: string(resp.Status),
RoleID: int64(resp.RoleID), RoleID: int64(resp.RoleID),
RoleName: resp.RoleName, RoleName: resp.RoleName,
PartnerID: resp.PartnerID, PartnerID: resp.PartnerID,
BranchName: resp.BranchName, PartnerName: resp.PartnerName,
CreatedAt: resp.CreatedAt.Format(time.RFC3339), CreatedAt: resp.CreatedAt.Format(time.RFC3339),
UpdatedAt: resp.CreatedAt.Format(time.RFC3339), UpdatedAt: resp.CreatedAt.Format(time.RFC3339),
} }
} }

View File

@ -26,6 +26,36 @@ type Partner struct {
Status string `json:"status"` Status string `json:"status"`
} }
type CreatePartnerRequest struct {
Name string `json:"name" validate:"required"`
Address string `json:"address"`
Username string `json:"username" validate:"required"`
FullName string `json:"full_name"`
Email string `json:"email"`
Password string `json:"password" validate:"required"`
NIK string `json:"nik"`
PhoneNumber string `json:"phone_number"`
BankName string `json:"bank_name"`
BankAccountNumber string `json:"bank_account_number"`
BankAccountHolderName string `json:"bank_account_holder_name"`
}
func (e *CreatePartnerRequest) ToEntity() *entity.CreatePartnerRequest {
return &entity.CreatePartnerRequest{
Name: e.Name,
Address: e.Address,
Username: e.Username,
FullName: e.FullName,
Email: e.Email,
Password: e.Password,
NIK: e.NIK,
PhoneNumber: e.PhoneNumber,
BankName: e.BankName,
BankAccountNumber: e.BankAccountNumber,
BankAccountHolderName: e.BankAccountHolderName,
}
}
func (e *Partner) ToEntity() *entity.Partner { func (e *Partner) ToEntity() *entity.Partner {
return &entity.Partner{ return &entity.Partner{
Name: e.Name, Name: e.Name,

View File

@ -28,14 +28,15 @@ func (p *ProductParam) ToEntity() entity.ProductSearch {
} }
type Product struct { type Product struct {
Name string `json:"name" validate:"required"` ID int64 `json:"id,omitempty"`
Type product.ProductType `json:"type" validate:"required"` PartnerID int64 `json:"partner_id"`
Price float64 `json:"price" validate:"required"` Name string `json:"name" validate:"required"`
Status product.ProductStatus `json:"status" validate:"required"` Type string `json:"type"`
Description string `json:"description" ` Price float64 `json:"price" validate:"required"`
Image string `json:"image" ` IsWeekendTicket bool `json:"is_weekend_ticket"`
BranchID int64 `json:"branch_id" validate:"required"` IsSeasonTicket bool `json:"is_season_ticket"`
StockQty int64 `json:"stock_qty" ` Status string `json:"status"`
Description string `json:"description" validate:"required"`
} }
func (e *Product) ToEntity() *entity.Product { func (e *Product) ToEntity() *entity.Product {
@ -45,8 +46,5 @@ func (e *Product) ToEntity() *entity.Product {
Price: e.Price, Price: e.Price,
Status: e.Status, Status: e.Status,
Description: e.Description, Description: e.Description,
Image: e.Image,
BranchID: e.BranchID,
StockQty: e.StockQty,
} }
} }

View File

@ -0,0 +1,75 @@
package request
import (
"furtuna-be/internal/entity"
)
type Site struct {
ID int64 `json:"id"`
Name string `json:"name" validate:"required"`
PartnerID *int64 `json:"partner_id"`
Image string `json:"image"`
Address string `json:"address"`
LocationLink string `json:"location_link"`
Description string `json:"description"`
Highlight string `json:"highlight"`
ContactPerson string `json:"contact_person"`
TnC string `json:"tnc"`
AdditionalInfo string `json:"additional_info"`
Status string `json:"status"`
IsSeasonTicket bool `json:"is_season_ticket"`
IsDiscountActive bool `json:"is_discount_active"`
Products []Product `json:"products"`
}
func (r *Site) ToEntity(createdBy int64) *entity.Site {
var products []entity.Product
for _, p := range r.Products {
products = append(products, entity.Product{
ID: p.ID,
PartnerID: *r.PartnerID,
Name: p.Name,
Type: p.Type,
Price: p.Price,
IsWeekendTicket: p.IsWeekendTicket,
IsSeasonTicket: p.IsSeasonTicket,
Status: p.Status,
Description: p.Description,
CreatedBy: createdBy,
})
}
return &entity.Site{
ID: r.ID,
Name: r.Name,
PartnerID: *r.PartnerID,
Image: r.Image,
Address: r.Address,
LocationLink: r.LocationLink,
Description: r.Description,
Highlight: r.Highlight,
ContactPerson: r.ContactPerson,
TnC: r.TnC,
AdditionalInfo: r.AdditionalInfo,
Status: r.Status,
IsSeasonTicket: r.IsSeasonTicket,
IsDiscountActive: r.IsDiscountActive,
Products: products,
}
}
type SiteParam struct {
Search string `form:"search"`
Name string `form:"name"`
Limit int `form:"limit,default=10"`
Offset int `form:"offset,default=0"`
}
func (r *SiteParam) ToEntity() entity.SiteSearch {
return entity.SiteSearch{
Search: r.Search,
Name: r.Name,
Limit: r.Limit,
Offset: r.Offset,
}
}

View File

@ -1,10 +1,10 @@
package response package response
type LoginResponse struct { type LoginResponse struct {
Token string `json:"token"` Token string `json:"token"`
Name string `json:"name"` Name string `json:"name"`
Role Role `json:"role"` Role Role `json:"role"`
Branch *Branch `json:"branch"` Partner *Partner `json:"partner"`
} }
type Role struct { type Role struct {

View File

@ -4,9 +4,9 @@ type Partner struct {
ID *int64 `json:"id"` ID *int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` Status string `json:"status"`
Address string `json:"address"` Address string `json:"address,omitempty"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at"` UpdatedAt string `json:"updated_at,omitempty"`
} }
type PartnerList struct { type PartnerList struct {

View File

@ -1,19 +1,18 @@
package response package response
import "furtuna-be/internal/constants/product"
type Product struct { type Product struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` PartnerID int64 `json:"partner_id"`
Type product.ProductType `json:"type"` SiteID int64 `json:"site_id"`
Price float64 `json:"price"` Name string `json:"name"`
Status product.ProductStatus `json:"status"` Type string `json:"type"`
Description string `json:"description" ` Price float64 `json:"price"`
Image string `json:"image" ` IsWeekendTicket bool `json:"is_weekend_ticket"`
BranchID int64 `json:"branch_id"` IsSeasonTicket bool `json:"is_season_ticket"`
StockQty int64 `json:"stock_qty"` Status string `json:"status"`
CreatedAt string `json:"created_at"` Description string `json:"description"`
UpdatedAt string `json:"updated_at"` CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} }
type ProductList struct { type ProductList struct {

View File

@ -0,0 +1,27 @@
package response
type Site struct {
ID *int64 `json:"id"`
Name string `json:"name"`
PartnerID int64 `json:"partner_id"`
Image string `json:"image"`
Address string `json:"address"`
LocationLink string `json:"location_link"`
Description string `json:"description"`
Highlight string `json:"highlight"`
ContactPerson string `json:"contact_person"`
TnC string `json:"tnc"`
AdditionalInfo string `json:"additional_info"`
Status string `json:"status"`
IsSeasonTicket bool `json:"is_season_ticket"`
IsDiscountActive bool `json:"is_discount_active"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type SiteList struct {
Sites []Site `json:"sites"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}

View File

@ -1,16 +1,16 @@
package response package response
type User struct { type User struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Status string `json:"status"` Status string `json:"status"`
RoleID int64 `json:"role_id"` RoleID int64 `json:"role_id"`
RoleName string `json:"role_name"` RoleName string `json:"role_name"`
PartnerID *int64 `json:"partner_id"` PartnerID *int64 `json:"partner_id"`
BranchName string `json:"partner_name"` PartnerName string `json:"partner_name"`
CreatedAt string `json:"created_at,omitempty"` CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"`
} }
type UserList struct { type UserList struct {

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
@ -27,13 +26,17 @@ func (r *AuthRepository) CheckExistsUserAccount(ctx context.Context, email strin
err := r.db. err := r.db.
Table("users"). Table("users").
Select("users.*, user_roles.role_id, user_roles.partner_id, roles.role_name, partners.name as partner_name"). Select("users.*, user_roles.role_id, user_roles.partner_id, roles.role_name, partners.name as partner_name, partners.status as partner_status").
Where("users.email = ?", email). Where("users.email = ?", email).
Joins("left join user_roles on users.id = user_roles.user_id"). Joins("left join user_roles on users.id = user_roles.user_id").
Joins("left join roles on user_roles.role_id = roles.role_id"). Joins("left join roles on user_roles.role_id = roles.role_id").
Joins("left join partners on user_roles.partner_id = partners.id"). Joins("left join partners on user_roles.partner_id = partners.id").
First(&user).Error First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("user with email %s does not exist", email) // or use a custom error type return nil, fmt.Errorf("user with email %s does not exist", email) // or use a custom error type

View File

@ -44,9 +44,9 @@ func (c *CryptoImpl) ValidateWT(tokenString string) (*jwt.Token, error) {
} }
func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) { func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) {
branchID := int64(0) partnerID := int64(0)
if user.PartnerID != nil { if user.PartnerID != nil {
branchID = *user.PartnerID partnerID = *user.PartnerID
} }
claims := &entity.JWTAuthClaims{ claims := &entity.JWTAuthClaims{
@ -56,11 +56,11 @@ func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) {
IssuedAt: time.Now().Unix(), IssuedAt: time.Now().Unix(),
NotBefore: time.Now().Unix(), NotBefore: time.Now().Unix(),
}, },
UserID: user.ID, UserID: user.ID,
Name: user.Name, Name: user.Name,
Email: user.Email, Email: user.Email,
Role: int(user.RoleID), Role: int(user.RoleID),
BranchID: branchID, PartnerID: partnerID,
} }
token, err := jwt. token, err := jwt.

View File

@ -28,6 +28,15 @@ func (b *PartnerRepository) Create(ctx context.Context, Partner *entity.PartnerD
return Partner, nil return Partner, nil
} }
func (b *PartnerRepository) CreateWithTx(ctx context.Context, tx *gorm.DB, Partner *entity.PartnerDB) (*entity.PartnerDB, error) {
err := tx.Create(Partner).Error
if err != nil {
logger.ContextLogger(ctx).Error("error when create Partner", zap.Error(err))
return nil, err
}
return Partner, nil
}
func (b *PartnerRepository) Update(ctx context.Context, Partner *entity.PartnerDB) (*entity.PartnerDB, error) { func (b *PartnerRepository) Update(ctx context.Context, Partner *entity.PartnerDB) (*entity.PartnerDB, error) {
if err := b.db.Save(Partner).Error; err != nil { if err := b.db.Save(Partner).Error; err != nil {
logger.ContextLogger(ctx).Error("error when update Partner", zap.Error(err)) logger.ContextLogger(ctx).Error("error when update Partner", zap.Error(err))

View File

@ -2,14 +2,17 @@ package repository
import ( import (
"context" "context"
"database/sql"
"furtuna-be/internal/repository/branches" "furtuna-be/internal/repository/branches"
"furtuna-be/internal/repository/orders" "furtuna-be/internal/repository/orders"
"furtuna-be/internal/repository/oss" "furtuna-be/internal/repository/oss"
"furtuna-be/internal/repository/partners" "furtuna-be/internal/repository/partners"
"furtuna-be/internal/repository/products" "furtuna-be/internal/repository/products"
"furtuna-be/internal/repository/sites"
"furtuna-be/internal/repository/studios" "furtuna-be/internal/repository/studios"
"furtuna-be/internal/repository/trx"
"furtuna-be/internal/repository/users" "furtuna-be/internal/repository/users"
repository "furtuna-be/internal/repository/wallet"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"gorm.io/gorm" "gorm.io/gorm"
@ -31,6 +34,9 @@ type RepoManagerImpl struct {
Order Order Order Order
OSS OSSRepository OSS OSSRepository
Partner PartnerRepository Partner PartnerRepository
Site SiteRepository
Trx TransactionManager
Wallet WalletRepository
} }
func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
@ -45,6 +51,9 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
Order: orders.NewOrderRepository(db), Order: orders.NewOrderRepository(db),
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig), OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
Partner: partners.NewPartnerRepository(db), Partner: partners.NewPartnerRepository(db),
Site: sites.NewSiteRepository(db),
Trx: trx.NewGormTransactionManager(db),
Wallet: repository.NewWalletRepository(db),
} }
} }
@ -69,6 +78,7 @@ type Crypto interface {
type User interface { type User interface {
Create(ctx context.Context, user *entity.UserDB) (*entity.UserDB, error) Create(ctx context.Context, user *entity.UserDB) (*entity.UserDB, error)
CreateWithTx(ctx context.Context, tx *gorm.DB, user *entity.UserDB) (*entity.UserDB, error)
GetAllUsers(ctx context.Context, req entity.UserSearch) (entity.UserList, int, error) GetAllUsers(ctx context.Context, req entity.UserSearch) (entity.UserList, int, error)
GetUserByID(ctx context.Context, id int64) (*entity.UserDB, error) GetUserByID(ctx context.Context, id int64) (*entity.UserDB, error)
GetUserByEmail(ctx context.Context, email string) (*entity.UserDB, error) GetUserByEmail(ctx context.Context, email string) (*entity.UserDB, error)
@ -115,8 +125,28 @@ type OSSRepository interface {
type PartnerRepository interface { type PartnerRepository interface {
Create(ctx context.Context, branch *entity.PartnerDB) (*entity.PartnerDB, error) Create(ctx context.Context, branch *entity.PartnerDB) (*entity.PartnerDB, error)
CreateWithTx(ctx context.Context, tx *gorm.DB, Partner *entity.PartnerDB) (*entity.PartnerDB, error)
Update(ctx context.Context, branch *entity.PartnerDB) (*entity.PartnerDB, error) Update(ctx context.Context, branch *entity.PartnerDB) (*entity.PartnerDB, error)
GetByID(ctx context.Context, id int64) (*entity.PartnerDB, error) GetByID(ctx context.Context, id int64) (*entity.PartnerDB, error)
GetAll(ctx context.Context, req entity.PartnerSearch) (entity.PartnerList, int, error) GetAll(ctx context.Context, req entity.PartnerSearch) (entity.PartnerList, int, error)
Delete(ctx context.Context, id int64) error Delete(ctx context.Context, id int64) error
} }
type SiteRepository interface {
Upsert(ctx context.Context, site *entity.Site) (*entity.Site, error)
Create(ctx context.Context, branch *entity.SiteDB) (*entity.SiteDB, error)
Update(ctx context.Context, branch *entity.SiteDB) (*entity.SiteDB, error)
GetByID(ctx context.Context, id int64) (*entity.SiteDB, error)
GetAll(ctx context.Context, req entity.SiteSearch) (entity.SiteList, int, error)
Delete(ctx context.Context, id int64) error
}
type TransactionManager interface {
Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error)
Commit(session *gorm.DB) *gorm.DB
Rollback(session *gorm.DB) *gorm.DB
}
type WalletRepository interface {
Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
}

View File

@ -0,0 +1,131 @@
package sites
import (
"context"
"furtuna-be/internal/common/logger"
"furtuna-be/internal/entity"
"go.uber.org/zap"
"gorm.io/gorm"
)
type SiteRepository struct {
db *gorm.DB
}
func NewSiteRepository(db *gorm.DB) *SiteRepository {
return &SiteRepository{
db: db,
}
}
func (r *SiteRepository) Upsert(ctx context.Context, site *entity.Site) (*entity.Site, error) {
err := r.db.Transaction(func(tx *gorm.DB) error {
if site.ID != 0 {
// Update site
if err := tx.Save(site).Error; err != nil {
return err
}
} else {
// Create new site
if err := tx.Create(site).Error; err != nil {
return err
}
}
if len(site.Products) > 0 {
for i := range site.Products {
site.Products[i].SiteID = site.ID
if site.Products[i].ID != 0 {
// Update existing product
if err := tx.Save(&site.Products[i]).Error; err != nil {
return err
}
} else {
// Create new product
if err := tx.Create(&site.Products[i]).Error; err != nil {
return err
}
}
}
}
return nil
})
if err != nil {
logger.ContextLogger(ctx).Error("error when upserting site", zap.Error(err))
return nil, err
}
return site, nil
}
func (r *SiteRepository) Create(ctx context.Context, site *entity.SiteDB) (*entity.SiteDB, error) {
err := r.db.Create(site).Error
if err != nil {
logger.ContextLogger(ctx).Error("error when creating site", zap.Error(err))
return nil, err
}
return site, nil
}
func (r *SiteRepository) Update(ctx context.Context, site *entity.SiteDB) (*entity.SiteDB, error) {
if err := r.db.Save(site).Error; err != nil {
logger.ContextLogger(ctx).Error("error when updating site", zap.Error(err))
return nil, err
}
return site, nil
}
func (r *SiteRepository) GetByID(ctx context.Context, id int64) (*entity.SiteDB, error) {
site := new(entity.SiteDB)
if err := r.db.First(site, id).Error; err != nil {
logger.ContextLogger(ctx).Error("error when getting site by ID", zap.Error(err))
return nil, err
}
return site, nil
}
func (r *SiteRepository) GetAll(ctx context.Context, req entity.SiteSearch) (entity.SiteList, int, error) {
var sites []*entity.SiteDB
var total int64
query := r.db
query = query.Where("deleted_at IS NULL")
if req.Search != "" {
query = query.Where("name ILIKE ?", "%"+req.Search+"%")
}
if req.Name != "" {
query = query.Where("name ILIKE ?", "%"+req.Name+"%")
}
if req.Limit > 0 {
query = query.Limit(req.Limit)
}
if req.Offset > 0 {
query = query.Offset(req.Offset)
}
if err := query.Find(&sites).Error; err != nil {
logger.ContextLogger(ctx).Error("error when getting all sites", zap.Error(err))
return nil, 0, err
}
if err := r.db.Model(&entity.SiteDB{}).Where(query).Count(&total).Error; err != nil {
logger.ContextLogger(ctx).Error("error when counting sites", zap.Error(err))
return nil, 0, err
}
return sites, int(total), nil
}
func (r *SiteRepository) Delete(ctx context.Context, id int64) error {
site := new(entity.SiteDB)
site.ID = id
if err := r.db.Delete(site).Error; err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,33 @@
package trx
import (
"context"
"database/sql"
"gorm.io/gorm"
)
type GormTransactionManager struct {
db *gorm.DB
}
func NewGormTransactionManager(db *gorm.DB) *GormTransactionManager {
return &GormTransactionManager{
db: db,
}
}
func (tm *GormTransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) {
tx := tm.db.Begin(opts...)
if tx.Error != nil {
return nil, tx.Error
}
return tx, nil
}
func (tm *GormTransactionManager) Commit(tx *gorm.DB) *gorm.DB {
return tx.Commit()
}
func (tm *GormTransactionManager) Rollback(tx *gorm.DB) *gorm.DB {
return tx.Rollback()
}

View File

@ -51,6 +51,29 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.UserDB) (*enti
return user, nil return user, nil
} }
func (r *UserRepository) CreateWithTx(ctx context.Context, tx *gorm.DB, user *entity.UserDB) (*entity.UserDB, error) {
user.ID = 0
if err := tx.Select("name", "email", "password", "status", "created_by", "nik", "user_type", "phone_number").Create(user).Error; err != nil {
logError(ctx, "creating user", err)
return nil, err
}
if err := tx.First(user, user.ID).Error; err != nil {
tx.Rollback()
logError(ctx, "retrieving user", err)
return nil, err
}
userRole := user.ToUserRoleDB()
if err := tx.Create(userRole).Error; err != nil {
tx.Rollback()
logError(ctx, "creating user role", err)
return nil, err
}
return user, nil
}
func (b *UserRepository) GetAllUsers(ctx context.Context, req entity.UserSearch) (entity.UserList, int, error) { func (b *UserRepository) GetAllUsers(ctx context.Context, req entity.UserSearch) (entity.UserList, int, error) {
var users []*entity.UserDB var users []*entity.UserDB
var total int64 var total int64

View File

@ -0,0 +1,65 @@
package repository
import (
"context"
"furtuna-be/internal/common/logger"
"furtuna-be/internal/entity"
"go.uber.org/zap"
"gorm.io/gorm"
)
type WalletRepository struct {
db *gorm.DB
}
func NewWalletRepository(db *gorm.DB) *WalletRepository {
return &WalletRepository{
db: db,
}
}
func (r *WalletRepository) Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) {
err := tx.Create(wallet).Error
if err != nil {
logger.ContextLogger(ctx).Error("error when creating wallet", zap.Error(err))
return nil, err
}
return wallet, nil
}
func (r *WalletRepository) Update(ctx context.Context, wallet *entity.Wallet) (*entity.Wallet, error) {
if err := r.db.Save(wallet).Error; err != nil {
logger.ContextLogger(ctx).Error("error when updating wallet", zap.Error(err))
return nil, err
}
return wallet, nil
}
func (r *WalletRepository) GetByID(ctx context.Context, id int64) (*entity.Wallet, error) {
wallet := new(entity.Wallet)
if err := r.db.First(wallet, id).Error; err != nil {
logger.ContextLogger(ctx).Error("error when getting wallet by id", zap.Error(err))
return nil, err
}
return wallet, nil
}
func (r *WalletRepository) GetAll(ctx context.Context) ([]*entity.Wallet, error) {
var wallets []*entity.Wallet
if err := r.db.Find(&wallets).Error; err != nil {
logger.ContextLogger(ctx).Error("error when getting all wallets", zap.Error(err))
return nil, err
}
return wallets, nil
}
func (r *WalletRepository) Delete(ctx context.Context, id int64) error {
wallet := new(entity.Wallet)
wallet.ID = id
if err := r.db.Delete(wallet).Error; err != nil {
logger.ContextLogger(ctx).Error("error when deleting wallet", zap.Error(err))
return err
}
return nil
}

View File

@ -6,6 +6,7 @@ import (
"furtuna-be/internal/handlers/http/oss" "furtuna-be/internal/handlers/http/oss"
"furtuna-be/internal/handlers/http/partner" "furtuna-be/internal/handlers/http/partner"
"furtuna-be/internal/handlers/http/product" "furtuna-be/internal/handlers/http/product"
site "furtuna-be/internal/handlers/http/sites"
"furtuna-be/internal/handlers/http/studio" "furtuna-be/internal/handlers/http/studio"
"furtuna-be/internal/handlers/http/user" "furtuna-be/internal/handlers/http/user"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
@ -52,6 +53,7 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana
order.NewHandler(serviceManager.OrderSvc), order.NewHandler(serviceManager.OrderSvc),
oss.NewOssHandler(serviceManager.OSSSvc), oss.NewOssHandler(serviceManager.OSSSvc),
partner.NewHandler(serviceManager.PartnerSvc), partner.NewHandler(serviceManager.PartnerSvc),
site.NewHandler(serviceManager.SiteSvc),
} }
for _, handler := range serverRoutes { for _, handler := range serverRoutes {

View File

@ -6,31 +6,68 @@ import (
"furtuna-be/internal/common/mycontext" "furtuna-be/internal/common/mycontext"
"furtuna-be/internal/entity" "furtuna-be/internal/entity"
"furtuna-be/internal/repository" "furtuna-be/internal/repository"
"furtuna-be/internal/services/users"
"go.uber.org/zap" "go.uber.org/zap"
) )
type PartnerService struct { type PartnerService struct {
repo repository.PartnerRepository repo repository.PartnerRepository
trx repository.TransactionManager
userSvc *users.UserService
walletRepo repository.WalletRepository
} }
func NewPartnerService(repo repository.PartnerRepository) *PartnerService { func NewPartnerService(repo repository.PartnerRepository,
userSvc *users.UserService, repoManager repository.TransactionManager,
walletRepo repository.WalletRepository) *PartnerService {
return &PartnerService{ return &PartnerService{
repo: repo, repo: repo,
userSvc: userSvc,
trx: repoManager,
walletRepo: walletRepo,
} }
} }
func (s *PartnerService) Create(ctx mycontext.Context, PartnerReq *entity.Partner) (*entity.Partner, error) { func (s *PartnerService) Create(ctx mycontext.Context, partnerReq *entity.CreatePartnerRequest) (*entity.Partner, error) {
PartnerDB := PartnerReq.ToPartnerDB() var err error
PartnerDB.CreatedBy = ctx.RequestedBy()
PartnerDB, err := s.repo.Create(ctx, PartnerDB) tx, err := s.trx.Begin(ctx)
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("error when create Partner", zap.Error(err)) logger.ContextLogger(ctx).Error("error when starting transaction", zap.Error(err))
return nil, err return nil, err
} }
return PartnerDB.ToPartner(), nil defer func() {
if r := recover(); r != nil {
s.trx.Rollback(tx)
panic(r)
} else if err != nil {
s.trx.Rollback(tx)
} else {
err = s.trx.Commit(tx).Error
}
}()
// Create Partner
partnerDB := partnerReq.ToPartnerDB(ctx.RequestedBy())
if partnerDB, err = s.repo.CreateWithTx(ctx, tx, partnerDB); err != nil {
logger.ContextLogger(ctx).Error("error when creating partner", zap.Error(err))
return nil, err
}
adminUser := partnerReq.ToUserAdmin(partnerDB.ID)
if adminUser, err = s.userSvc.CreateWithTx(ctx, tx, adminUser); err != nil {
logger.ContextLogger(ctx).Error("error when creating admin user", zap.Error(err))
return nil, err
}
partnerWallet := partnerReq.ToWallet(partnerDB.ID)
if partnerWallet, err = s.walletRepo.Create(ctx, tx, partnerWallet); err != nil {
logger.ContextLogger(ctx).Error("error when creating wallet", zap.Error(err))
return nil, err
}
return partnerDB.ToPartner(), nil
} }
func (s *PartnerService) Update(ctx mycontext.Context, id int64, PartnerReq *entity.Partner) (*entity.Partner, error) { func (s *PartnerService) Update(ctx mycontext.Context, id int64, PartnerReq *entity.Partner) (*entity.Partner, error) {

View File

@ -8,8 +8,10 @@ import (
"furtuna-be/internal/services/oss" "furtuna-be/internal/services/oss"
"furtuna-be/internal/services/partner" "furtuna-be/internal/services/partner"
"furtuna-be/internal/services/product" "furtuna-be/internal/services/product"
"furtuna-be/internal/services/sites"
"furtuna-be/internal/services/studio" "furtuna-be/internal/services/studio"
"furtuna-be/internal/services/users" "furtuna-be/internal/services/users"
"gorm.io/gorm"
"furtuna-be/config" "furtuna-be/config"
"furtuna-be/internal/entity" "furtuna-be/internal/entity"
@ -28,6 +30,7 @@ type ServiceManagerImpl struct {
OrderSvc Order OrderSvc Order
OSSSvc OSSService OSSSvc OSSService
PartnerSvc Partner PartnerSvc Partner
SiteSvc Site
} }
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl { func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
@ -40,7 +43,9 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
ProductSvc: product.NewProductService(repo.Product), ProductSvc: product.NewProductService(repo.Product),
OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Studio), OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Studio),
OSSSvc: oss.NewOSSService(repo.OSS), OSSSvc: oss.NewOSSService(repo.OSS),
PartnerSvc: partner.NewPartnerService(repo.Partner), PartnerSvc: partner.NewPartnerService(
repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet),
SiteSvc: site.NewSiteService(repo.Site),
} }
} }
@ -58,6 +63,7 @@ type Event interface {
type User interface { type User interface {
Create(ctx mycontext.Context, userReq *entity.User) (*entity.User, error) Create(ctx mycontext.Context, userReq *entity.User) (*entity.User, error)
CreateWithTx(ctx mycontext.Context, tx *gorm.DB, userReq *entity.User) (*entity.User, error)
GetAll(ctx mycontext.Context, search entity.UserSearch) ([]*entity.User, int, error) GetAll(ctx mycontext.Context, search entity.UserSearch) ([]*entity.User, int, error)
Update(ctx mycontext.Context, id int64, userReq *entity.User) (*entity.User, error) Update(ctx mycontext.Context, id int64, userReq *entity.User) (*entity.User, error)
GetByID(ctx mycontext.Context, id int64) (*entity.User, error) GetByID(ctx mycontext.Context, id int64) (*entity.User, error)
@ -102,9 +108,17 @@ type OSSService interface {
} }
type Partner interface { type Partner interface {
Create(ctx mycontext.Context, branchReq *entity.Partner) (*entity.Partner, error) Create(ctx mycontext.Context, branchReq *entity.CreatePartnerRequest) (*entity.Partner, error)
Update(ctx mycontext.Context, id int64, branchReq *entity.Partner) (*entity.Partner, error) Update(ctx mycontext.Context, id int64, branchReq *entity.Partner) (*entity.Partner, error)
GetByID(ctx context.Context, id int64) (*entity.Partner, error) GetByID(ctx context.Context, id int64) (*entity.Partner, error)
GetAll(ctx context.Context, search entity.PartnerSearch) ([]*entity.Partner, int, error) GetAll(ctx context.Context, search entity.PartnerSearch) ([]*entity.Partner, int, error)
Delete(ctx mycontext.Context, id int64) error Delete(ctx mycontext.Context, id int64) error
} }
type Site interface {
Create(ctx mycontext.Context, branchReq *entity.Site) (*entity.Site, error)
Update(ctx mycontext.Context, id int64, branchReq *entity.Site) (*entity.Site, error)
GetByID(ctx context.Context, id int64) (*entity.Site, error)
GetAll(ctx context.Context, search entity.SiteSearch) ([]*entity.Site, int, error)
Delete(ctx mycontext.Context, id int64) error
}

View File

@ -0,0 +1,89 @@
package site
import (
"context"
"furtuna-be/internal/common/logger"
"furtuna-be/internal/common/mycontext"
"furtuna-be/internal/entity"
"furtuna-be/internal/repository"
"go.uber.org/zap"
)
type SiteService struct {
repo repository.SiteRepository
}
func NewSiteService(repo repository.SiteRepository) *SiteService {
return &SiteService{
repo: repo,
}
}
func (s *SiteService) Create(ctx mycontext.Context, siteReq *entity.Site) (*entity.Site, error) {
siteDB := siteReq
siteDB.CreatedBy = ctx.RequestedBy()
siteDB, err := s.repo.Upsert(ctx, siteDB)
if err != nil {
logger.ContextLogger(ctx).Error("error when creating site", zap.Error(err))
return nil, err
}
return siteDB, nil
}
func (s *SiteService) Update(ctx mycontext.Context, id int64, siteReq *entity.Site) (*entity.Site, error) {
existingSite, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
existingSite.ToUpdatedSite(ctx.RequestedBy(), *siteReq)
updatedSiteDB, err := s.repo.Update(ctx, existingSite.ToSiteDB())
if err != nil {
logger.ContextLogger(ctx).Error("error when updating site", zap.Error(err))
return nil, err
}
return updatedSiteDB.ToSite(), nil
}
func (s *SiteService) GetByID(ctx context.Context, id int64) (*entity.Site, error) {
siteDB, err := s.repo.GetByID(ctx, id)
if err != nil {
logger.ContextLogger(ctx).Error("error when getting site by ID", zap.Error(err))
return nil, err
}
return siteDB.ToSite(), nil
}
func (s *SiteService) GetAll(ctx context.Context, search entity.SiteSearch) ([]*entity.Site, int, error) {
sites, total, err := s.repo.GetAll(ctx, search)
if err != nil {
logger.ContextLogger(ctx).Error("error when getting all sites", zap.Error(err))
return nil, 0, err
}
return sites.ToSiteList(), total, nil
}
func (s *SiteService) Delete(ctx mycontext.Context, id int64) error {
siteDB, err := s.repo.GetByID(ctx, id)
if err != nil {
logger.ContextLogger(ctx).Error("error when getting site by ID", zap.Error(err))
return err
}
siteDB.SetDeleted(ctx.RequestedBy())
_, err = s.repo.Update(ctx, siteDB)
if err != nil {
logger.ContextLogger(ctx).Error("error when updating site", zap.Error(err))
return err
}
return nil
}

View File

@ -3,6 +3,7 @@ package users
import ( import (
"fmt" "fmt"
"furtuna-be/internal/common/mycontext" "furtuna-be/internal/common/mycontext"
"gorm.io/gorm"
"go.uber.org/zap" "go.uber.org/zap"
@ -48,6 +49,31 @@ func (s *UserService) Create(ctx mycontext.Context, userReq *entity.User) (*enti
return userDB.ToUser(), nil return userDB.ToUser(), nil
} }
func (s *UserService) CreateWithTx(ctx mycontext.Context, tx *gorm.DB, userReq *entity.User) (*entity.User, error) {
//check
userExist, err := s.repo.GetUserByEmail(ctx, userReq.Email)
if err != nil {
return nil, err
}
if userExist != nil {
return nil, fmt.Errorf("Email already exist")
}
userDB, err := userReq.ToUserDB(ctx.RequestedBy())
if err != nil {
return nil, err
}
userDB, err = s.repo.CreateWithTx(ctx, tx, userDB)
if err != nil {
logger.ContextLogger(ctx).Error("error when create user", zap.Error(err))
return nil, err
}
return userDB.ToUser(), nil
}
func (s *UserService) GetAll(ctx mycontext.Context, search entity.UserSearch) ([]*entity.User, int, error) { func (s *UserService) GetAll(ctx mycontext.Context, search entity.UserSearch) ([]*entity.User, int, error) {
users, total, err := s.repo.GetAllUsers(ctx, search) users, total, err := s.repo.GetAllUsers(ctx, search)
@ -82,7 +108,7 @@ func (s *UserService) Update(ctx mycontext.Context, id int64, userReq *entity.Us
return nil, err return nil, err
} }
existingUser.BranchName = branch.Name existingUser.PartnerName = branch.Name
} }
err = existingUser.ToUpdatedUser(*userReq) err = existingUser.ToUpdatedUser(*userReq)

View File

@ -3,4 +3,9 @@ CREATE TABLE roles (
role_name VARCHAR(255) NOT NULL role_name VARCHAR(255) NOT NULL
); );
INSERT INTO roles(role_id, role_name) VALUES (1, 'super_admin'), (2, 'branch_admin'); INSERT INTO roles(role_id, role_name)
VALUES (1, 'super_admin'),
(2, 'admin'),
(3, 'admin_partner'),
(4, 'admin_site'),
(5, 'cashier');

View File

@ -1,12 +1,16 @@
CREATE TABLE partners CREATE TABLE partners
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
status varchar, status VARCHAR(50),
Address varchar, license_expired_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(), address VARCHAR(255),
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, bank_name VARCHAR(255),
created_by INTEGER, bank_account_number VARCHAR(50),
updated_by INTEGER, bank_account_holder_name VARCHAR(255),
deleted_at timestamp created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER,
updated_by INTEGER,
deleted_at TIMESTAMP
); );

View File

@ -1,15 +1,18 @@
CREATE TABLE products CREATE TABLE products
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
site_id INTEGER, partner_id INTEGER,
name VARCHAR(255) NOT NULL, site_id INTEGER,
type VARCHAR, name VARCHAR(255) NOT NULL,
prices INTEGER[], type VARCHAR,
status VARCHAR, price decimal,
description VARCHAR(255) NOT NULL, is_weekend_ticket boolean,
created_at TIMESTAMP NOT NULL DEFAULT NOW(), is_season_ticket boolean,
updated_at TIMESTAMP NOT NULL DEFAULT NOW(), status VARCHAR,
deleted_at TIMESTAMP, description VARCHAR(255) NOT NULL,
created_by INTEGER, created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_by INTEGER updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP,
created_by INTEGER,
updated_by INTEGER
); );

View File

View File

@ -0,0 +1,11 @@
CREATE TABLE public.wallets
(
id SERIAL PRIMARY KEY,
partner_id INTEGER NOT NULL,
balance DECIMAL(18, 2) NOT NULL DEFAULT 0.00,
currency VARCHAR(3) NOT NULL,
status VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
FOREIGN KEY (partner_id) REFERENCES partners (id) ON DELETE CASCADE
);