Dev / controllers /company_controller.go
Mhamdans17
feat: single session enforcement + UI improvements
c6e89e6
Raw
History Blame Contribute Delete
31.7 kB
package controllers
import (
"fmt"
"net/http"
"strconv"
"time"
"service-warungpos-go/config"
"service-warungpos-go/models"
"service-warungpos-go/services"
"github.com/gin-gonic/gin"
)
// DTOs & Custom Structs
type CompanyResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
BankAccountNumber string `json:"bank_account_number"`
BankName string `json:"bank_name"`
Address string `json:"address"`
Email string `json:"email"`
Phone string `json:"phone"`
LogoURL string `json:"logo_url"`
IsActive bool `json:"is_active"`
FeePercentage float64 `json:"fee_percentage"`
CashFeeRules string `json:"cash_fee_rules"`
QrisFeeRules string `json:"qris_fee_rules"`
QrisFeePercentage float64 `json:"qris_fee_percentage"`
QrisFeeFixed int `json:"qris_fee_fixed"`
QrisFeeMin int `json:"qris_fee_min"`
QrisMinTransaction int `json:"qris_min_transaction"`
AllowQrisBelowMin bool `json:"allow_qris_below_min"`
ChargeFeeBeforeDiscount bool `json:"charge_fee_before_discount"`
WalletBalance float64 `json:"wallet_balance"`
WalletLimit float64 `json:"wallet_limit"`
EnableMember bool `json:"enable_member"`
EnableVoucher bool `json:"enable_voucher"`
EnableMultiBranch bool `json:"enable_multi_branch"`
MaxBranches int `json:"max_branches"`
EnableMultiKasir bool `json:"enable_multi_kasir"`
MaxCashiers int `json:"max_cashiers"`
EnforceSingleSession bool `json:"enforce_single_session"`
ApprovedAt *time.Time `json:"approved_at"`
OwnerVerified *bool `json:"owner_verified,omitempty"`
OwnerName string `json:"owner_name,omitempty"`
OwnerEmail string `json:"owner_email,omitempty"`
OwnerPhone string `json:"owner_phone,omitempty"`
PointRules []models.PointRule `json:"point_rules"`
CreatedAt time.Time `json:"created_at"`
}
type CompanyCreateRequest struct {
Name string `json:"name" binding:"required"`
BankAccountNumber string `json:"bank_account_number"`
BankName string `json:"bank_name"`
Address string `json:"address"`
Email string `json:"email"`
Phone string `json:"phone"`
LogoURL string `json:"logo_url"`
FeePercentage float64 `json:"fee_percentage"`
TripayMerchantCode string `json:"tripay_merchant_code"`
TripayAPIKey string `json:"tripay_api_key"`
TripayPrivateKey string `json:"tripay_private_key"`
TripayProxyURL string `json:"tripay_proxy_url"`
}
type PointRuleInput struct {
MinAmount int `json:"min_amount" binding:"required"`
PointReward int `json:"point_reward" binding:"required"`
}
type UpdateCompanyPointsRequest struct {
PointRules []PointRuleInput `json:"point_rules"`
}
type BranchCreateRequest struct {
Name string `json:"name" binding:"required"`
Address string `json:"address"`
Phone string `json:"phone"`
IsPointEnabled *bool `json:"is_point_enabled"`
}
type BranchResponse struct {
ID uint `json:"id"`
CompanyID uint `json:"company_id"`
Name string `json:"name"`
Address string `json:"address"`
Phone string `json:"phone"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
}
type BranchProductResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
CategoryID *uint `json:"category_id"`
ImageURL string `json:"image_url"`
IsAvailable bool `json:"is_available"`
}
// ==================== COMPANY CONTROLLER ====================
func ListCompanies(c *gin.Context) {
limitStr := c.Query("limit")
skipStr := c.Query("skip")
searchQuery := c.Query("search")
query := config.DB.Model(&models.Company{}).
Select("id, name, bank_account_number, bank_name, address, email, phone, logo_url, is_active, fee_percentage, cash_fee_rules, qris_fee_rules, qris_fee_percentage, qris_fee_fixed, qris_fee_min, qris_min_transaction, allow_qris_below_min, charge_fee_before_discount, wallet_balance, wallet_limit, enable_member, enable_voucher, enable_multi_branch, max_branches, enable_multi_kasir, max_cashiers, enforce_single_session, approved_at, created_at")
if searchQuery != "" {
query = query.Where("name ILIKE ? OR email ILIKE ? OR phone ILIKE ? OR address ILIKE ?",
"%"+searchQuery+"%",
"%"+searchQuery+"%",
"%"+searchQuery+"%",
"%"+searchQuery+"%")
}
if limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil {
query = query.Limit(limit)
}
}
if skipStr != "" {
if skip, err := strconv.Atoi(skipStr); err == nil {
query = query.Offset(skip)
}
}
var companies []models.Company
if err := query.Preload("PointRules").Find(&companies).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data perusahaan"})
return
}
if len(companies) == 0 {
c.JSON(http.StatusOK, []CompanyResponse{})
return
}
var companyIDs []uint
for _, comp := range companies {
companyIDs = append(companyIDs, comp.ID)
}
// Ambil semua owner user dalam 1 query (hanya kolom yang diperlukan)
var owners []models.User
config.DB.Select("id, name, email, phone, company_id").
Where("company_id IN ? AND role = ?", companyIDs, "owner").
Find(&owners)
// Map company_id -> owner
ownerMap := make(map[uint]models.User)
var ownerPhones []string
for _, o := range owners {
if o.CompanyID != nil {
ownerMap[*o.CompanyID] = o
if o.Phone != "" {
ownerPhones = append(ownerPhones, o.Phone)
}
}
}
// Ambil status verifikasi OTP semua owner dalam 1 query
otpMap := make(map[string]bool)
if len(ownerPhones) > 0 {
var otps []models.OTP
config.DB.Select("phone_number").
Where("phone_number IN ? AND is_verified = ?", ownerPhones, true).
Find(&otps)
for _, otp := range otps {
otpMap[otp.PhoneNumber] = true
}
}
resp := []CompanyResponse{}
for _, comp := range companies {
owner, hasOwner := ownerMap[comp.ID]
var ownerVerified *bool
var ownerName, ownerEmail, ownerPhone string
if hasOwner {
ownerName = owner.Name
ownerEmail = owner.Email
ownerPhone = owner.Phone
if owner.Phone != "" {
isVerified := otpMap[owner.Phone]
ownerVerified = &isVerified
}
}
resp = append(resp, CompanyResponse{
ID: comp.ID,
Name: comp.Name,
BankAccountNumber: comp.BankAccountNumber,
BankName: comp.BankName,
Address: comp.Address,
Email: comp.Email,
Phone: comp.Phone,
LogoURL: comp.LogoURL,
IsActive: comp.IsActive,
FeePercentage: comp.FeePercentage,
ApprovedAt: comp.ApprovedAt,
OwnerVerified: ownerVerified,
OwnerName: ownerName,
OwnerEmail: ownerEmail,
OwnerPhone: ownerPhone,
PointRules: comp.PointRules,
CashFeeRules: comp.CashFeeRules,
QrisFeeRules: comp.QrisFeeRules,
QrisFeePercentage: comp.QrisFeePercentage,
QrisFeeFixed: comp.QrisFeeFixed,
QrisFeeMin: comp.QrisFeeMin,
QrisMinTransaction: comp.QrisMinTransaction,
AllowQrisBelowMin: comp.AllowQrisBelowMin,
ChargeFeeBeforeDiscount: comp.ChargeFeeBeforeDiscount,
WalletBalance: comp.WalletBalance,
WalletLimit: comp.WalletLimit,
EnableMember: comp.EnableMember,
EnableVoucher: comp.EnableVoucher,
EnableMultiBranch: comp.EnableMultiBranch,
MaxBranches: comp.MaxBranches,
EnableMultiKasir: comp.EnableMultiKasir,
MaxCashiers: comp.MaxCashiers,
EnforceSingleSession: comp.EnforceSingleSession,
CreatedAt: comp.CreatedAt,
})
}
c.JSON(http.StatusOK, resp)
}
func GetMyCompany(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
if user.CompanyID == nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Tidak terhubung ke perusahaan"})
return
}
var company models.Company
if err := config.DB.Preload("PointRules").First(&company, *user.CompanyID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Perusahaan tidak ditemukan"})
return
}
c.JSON(http.StatusOK, company)
}
func UpdateMyCompanyPoints(c *gin.Context) {
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Unauthorized"})
return
}
user := userVal.(*models.User)
if user.CompanyID == nil || user.Role != "owner" {
c.JSON(http.StatusForbidden, gin.H{"detail": "Hanya owner yang bisa mengatur poin"})
return
}
var req UpdateCompanyPointsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Data tidak valid"})
return
}
// Hapus point rules yang lama
config.DB.Where("company_id = ?", *user.CompanyID).Delete(&models.PointRule{})
// Masukkan point rules yang baru
var newRules []models.PointRule
for _, r := range req.PointRules {
newRules = append(newRules, models.PointRule{
CompanyID: *user.CompanyID,
MinAmount: r.MinAmount,
PointReward: r.PointReward,
})
}
if len(newRules) > 0 {
if err := config.DB.Create(&newRules).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan pengaturan poin"})
return
}
}
var updatedCompany models.Company
config.DB.Preload("PointRules").First(&updatedCompany, *user.CompanyID)
c.JSON(http.StatusOK, gin.H{"message": "Pengaturan poin berhasil disimpan", "company": updatedCompany})
}
func GetCompany(c *gin.Context) {
companyIDStr := c.Param("company_id")
companyID, err := strconv.Atoi(companyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"})
return
}
var company models.Company
if err := config.DB.First(&company, companyID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"})
return
}
c.JSON(http.StatusOK, company)
}
func CreateCompany(c *gin.Context) {
var req CompanyCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var count int64
config.DB.Model(&models.Company{}).Where("name = ?", req.Name).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Company name already exists"})
return
}
newComp := models.Company{
Name: req.Name,
BankAccountNumber: req.BankAccountNumber,
BankName: req.BankName,
Address: req.Address,
Email: req.Email,
Phone: req.Phone,
LogoURL: req.LogoURL,
IsActive: true,
FeePercentage: req.FeePercentage,
TripayMerchantCode: req.TripayMerchantCode,
TripayAPIKey: req.TripayAPIKey,
TripayPrivateKey: req.TripayPrivateKey,
TripayProxyURL: req.TripayProxyURL,
}
if err := config.DB.Create(&newComp).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal membuat perusahaan baru"})
return
}
c.JSON(http.StatusCreated, newComp)
}
func UpdateCompany(c *gin.Context) {
companyIDStr := c.Param("company_id")
companyID, err := strconv.Atoi(companyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"})
return
}
var req CompanyCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var company models.Company
if err := config.DB.First(&company, companyID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"})
return
}
if req.Name != "" {
var count int64
config.DB.Model(&models.Company{}).Where("name = ? AND id != ?", req.Name, companyID).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Company name already exists"})
return
}
company.Name = req.Name
}
company.BankAccountNumber = req.BankAccountNumber
company.BankName = req.BankName
company.Address = req.Address
company.Email = req.Email
company.Phone = req.Phone
company.LogoURL = req.LogoURL
company.FeePercentage = req.FeePercentage
company.TripayMerchantCode = req.TripayMerchantCode
company.TripayAPIKey = req.TripayAPIKey
company.TripayPrivateKey = req.TripayPrivateKey
company.TripayProxyURL = req.TripayProxyURL
if err := config.DB.Save(&company).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal meng-update perusahaan"})
return
}
c.JSON(http.StatusOK, company)
}
func ToggleActiveCompany(c *gin.Context) {
companyIDStr := c.Param("company_id")
companyID, err := strconv.Atoi(companyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"})
return
}
var company models.Company
if err := config.DB.First(&company, companyID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"})
return
}
tx := config.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
company.IsActive = !company.IsActive
tx.Save(&company)
// Ubah status keaktifan owner juga
var owner models.User
if err := tx.Where("company_id = ? AND role = ?", companyID, "owner").First(&owner).Error; err == nil {
owner.IsActive = company.IsActive
tx.Save(&owner)
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal melakukan toggle status"})
return
}
status := "dinonaktifkan"
if company.IsActive {
status = "diaktifkan"
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Perusahaan %s berhasil %s", company.Name, status),
"is_active": company.IsActive,
})
}
func DeleteCompany(c *gin.Context) {
companyIDStr := c.Param("company_id")
companyID, err := strconv.Atoi(companyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"})
return
}
var company models.Company
if err := config.DB.First(&company, companyID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"})
return
}
tx := config.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. Ambil semua user terkait company ini, lalu hapus record OTP mereka
var users []models.User
tx.Where("company_id = ?", companyID).Find(&users)
for _, u := range users {
if u.Phone != "" {
tx.Where("phone_number = ?", u.Phone).Delete(&models.OTP{})
}
}
// 2. Hapus order_items terlebih dahulu (menggunakan subquery) untuk menghindari FK violations
if err := tx.Exec("DELETE FROM order_items WHERE order_id IN (SELECT id FROM orders WHERE company_id = ?)", companyID).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data order items"})
return
}
// 3. Hapus orders
if err := tx.Where("company_id = ?", companyID).Delete(&models.Order{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data order"})
return
}
// 4. Hapus branch_products
if err := tx.Exec("DELETE FROM branch_products WHERE product_id IN (SELECT id FROM products WHERE company_id = ?)", companyID).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data branch products"})
return
}
// 5. Hapus products
if err := tx.Where("company_id = ?", companyID).Delete(&models.Product{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data produk"})
return
}
// 6. Hapus categories
if err := tx.Where("company_id = ?", companyID).Delete(&models.Category{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data kategori"})
return
}
// 7. Hapus users
if err := tx.Where("company_id = ?", companyID).Delete(&models.User{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data user"})
return
}
// 8. Hapus branches
if err := tx.Where("company_id = ?", companyID).Delete(&models.Branch{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data cabang"})
return
}
// 9. Terakhir hapus company utama
if err := tx.Delete(&company).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data perusahaan"})
return
}
tx.Commit()
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Perusahaan '%s' dan seluruh data terkait berhasil dihapus", company.Name),
})
}
func RejectCompany(c *gin.Context) {
companyIDStr := c.Param("company_id")
companyID, err := strconv.Atoi(companyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"})
return
}
var company models.Company
if err := config.DB.First(&company, companyID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"})
return
}
if company.IsActive {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Tidak bisa menolak perusahaan yang sudah aktif"})
return
}
tx := config.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. Ambil semua user terkait company ini, lalu hapus record OTP mereka
var users []models.User
tx.Where("company_id = ?", companyID).Find(&users)
for _, u := range users {
if u.Phone != "" {
tx.Where("phone_number = ?", u.Phone).Delete(&models.OTP{})
}
}
// 2. Hapus order_items terlebih dahulu (menggunakan subquery)
if err := tx.Exec("DELETE FROM order_items WHERE order_id IN (SELECT id FROM orders WHERE company_id = ?)", companyID).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data order items"})
return
}
// 3. Hapus orders
if err := tx.Where("company_id = ?", companyID).Delete(&models.Order{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data order"})
return
}
// 4. Hapus branch_products
if err := tx.Exec("DELETE FROM branch_products WHERE product_id IN (SELECT id FROM products WHERE company_id = ?)", companyID).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data branch products"})
return
}
// 5. Hapus products
if err := tx.Where("company_id = ?", companyID).Delete(&models.Product{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data produk"})
return
}
// 6. Hapus categories
if err := tx.Where("company_id = ?", companyID).Delete(&models.Category{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data kategori"})
return
}
// 7. Hapus users
if err := tx.Where("company_id = ?", companyID).Delete(&models.User{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data user"})
return
}
// 8. Hapus branches
if err := tx.Where("company_id = ?", companyID).Delete(&models.Branch{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data cabang"})
return
}
// 9. Terakhir hapus company utama
if err := tx.Delete(&company).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data perusahaan"})
return
}
tx.Commit()
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Pendaftaran '%s' ditolak dan dihapus dari sistem", company.Name),
})
}
func ApproveCompany(c *gin.Context) {
companyIDStr := c.Param("company_id")
companyID, err := strconv.Atoi(companyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"})
return
}
tx := config.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var company models.Company
if err := tx.First(&company, companyID).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"})
return
}
var owner models.User
if err := tx.Where("company_id = ? AND role = ?", companyID, "owner").First(&owner).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusBadRequest, gin.H{"detail": "Company ini belum memiliki Owner"})
return
}
now := time.Now()
company.IsActive = true
company.ApprovedAt = &now
tx.Save(&company)
owner.IsActive = true
tx.Save(&owner)
if err := tx.Commit().Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyetujui perusahaan"})
return
}
// Kirim Notifikasi WA (Async)
go services.SendApprovalWa(owner.Phone, company.Name, owner.Email)
c.JSON(http.StatusOK, gin.H{
"message": "Perusahaan dan owner berhasil disetujui",
})
}
// ==================== BRANCH CONTROLLER ====================
func ListBranches(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
branches := []models.Branch{}
if err := config.DB.Select("id, company_id, name, address, phone, is_active, is_point_enabled, created_at").Where("company_id = ?", *owner.CompanyID).Find(&branches).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data cabang"})
return
}
c.JSON(http.StatusOK, branches)
}
func CreateBranch(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
var req BranchCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var count int64
config.DB.Model(&models.Branch{}).Where("company_id = ? AND name = ?", *owner.CompanyID, req.Name).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Nama cabang sudah ada"})
return
}
var company models.Company
if err := config.DB.First(&company, *owner.CompanyID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data perusahaan"})
return
}
if !company.EnableMultiBranch && company.MaxBranches <= 0 {
var branchCount int64
config.DB.Model(&models.Branch{}).Where("company_id = ?", *owner.CompanyID).Count(&branchCount)
if branchCount >= 1 {
c.JSON(http.StatusForbidden, gin.H{"detail": "Paket perusahaan Anda tidak mendukung pembuatan lebih dari 1 cabang"})
return
}
} else if company.MaxBranches > 0 {
var branchCount int64
config.DB.Model(&models.Branch{}).Where("company_id = ?", *owner.CompanyID).Count(&branchCount)
if branchCount >= int64(company.MaxBranches) {
c.JSON(http.StatusForbidden, gin.H{"detail": fmt.Sprintf("Batas maksimal cabang (%d) telah tercapai untuk perusahaan Anda", company.MaxBranches)})
return
}
}
isPointEnabled := true
if req.IsPointEnabled != nil {
isPointEnabled = *req.IsPointEnabled
}
newBranch := models.Branch{
CompanyID: *owner.CompanyID,
Name: req.Name,
Address: req.Address,
Phone: req.Phone,
IsActive: true,
IsPointEnabled: isPointEnabled,
}
if err := config.DB.Create(&newBranch).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan cabang baru"})
return
}
c.JSON(http.StatusCreated, newBranch)
}
func UpdateBranch(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
branchIDStr := c.Param("branch_id")
branchID, err := strconv.Atoi(branchIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"})
return
}
var req BranchCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var branch models.Branch
if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"})
return
}
if req.Name != "" {
var count int64
config.DB.Model(&models.Branch{}).Where("company_id = ? AND name = ? AND id != ?", *owner.CompanyID, req.Name, branchID).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Nama cabang sudah ada"})
return
}
branch.Name = req.Name
}
branch.Address = req.Address
branch.Phone = req.Phone
if req.IsPointEnabled != nil {
branch.IsPointEnabled = *req.IsPointEnabled
}
if err := config.DB.Save(&branch).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal meng-update cabang"})
return
}
c.JSON(http.StatusOK, branch)
}
func DeleteBranch(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
branchIDStr := c.Param("branch_id")
branchID, err := strconv.Atoi(branchIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"})
return
}
var branch models.Branch
if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"})
return
}
branch.IsActive = false
config.DB.Save(&branch)
c.JSON(http.StatusOK, gin.H{"message": "Cabang dinonaktifkan"})
}
func ToggleBranchStatus(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
branchIDStr := c.Param("branch_id")
branchID, err := strconv.Atoi(branchIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"})
return
}
var branch models.Branch
if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"})
return
}
branch.IsActive = !branch.IsActive
config.DB.Save(&branch)
status := "dinonaktifkan"
if branch.IsActive {
status = "diaktifkan"
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Cabang berhasil %s", status),
"is_active": branch.IsActive,
})
}
func GetBranchProducts(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
branchIDStr := c.Param("branch_id")
branchID, err := strconv.Atoi(branchIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"})
return
}
// Pastikan cabang milik perusahaan owner
var branch models.Branch
if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"})
return
}
// Ambil semua produk aktif
var products []models.Product
if err := config.DB.Where("company_id = ? AND is_active = ?", *owner.CompanyID, true).Find(&products).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data produk"})
return
}
// Ambil relasi availability
var branchProducts []models.BranchProduct
config.DB.Where("branch_id = ?", branchID).Find(&branchProducts)
bpMap := make(map[uint]bool)
for _, bp := range branchProducts {
bpMap[bp.ProductID] = bp.IsAvailable
}
resp := []BranchProductResponse{}
for _, p := range products {
isAvailable, exists := bpMap[p.ID]
if !exists {
isAvailable = true // Default tersedia
}
resp = append(resp, BranchProductResponse{
ID: p.ID,
Name: p.Name,
Price: p.Price,
CategoryID: p.CategoryID,
ImageURL: p.ImageURL,
IsAvailable: isAvailable,
})
}
c.JSON(http.StatusOK, resp)
}
func ToggleProductAvailability(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
branchIDStr := c.Param("branch_id")
branchID, err := strconv.Atoi(branchIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"})
return
}
productIDStr := c.Param("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID produk tidak valid"})
return
}
var branch models.Branch
if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"})
return
}
var product models.Product
if err := config.DB.Where("id = ? AND company_id = ?", productID, *owner.CompanyID).First(&product).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Produk tidak ditemukan"})
return
}
var bp models.BranchProduct
err = config.DB.Where("branch_id = ? AND product_id = ?", branchID, productID).First(&bp).Error
if err == nil {
bp.IsAvailable = !bp.IsAvailable
config.DB.Save(&bp)
} else {
// Belum pernah diset -> default True, jadi toggle = False
bp = models.BranchProduct{
BranchID: uint(branchID),
ProductID: uint(productID),
IsAvailable: false,
}
config.DB.Create(&bp)
}
c.JSON(http.StatusOK, gin.H{
"message": "Status diperbarui",
"is_available": bp.IsAvailable,
})
}
type UpdateCompanySettingsRequest struct {
ChargeFeeBeforeDiscount *bool `json:"charge_fee_before_discount"`
EnableMember *bool `json:"enable_member"`
EnableVoucher *bool `json:"enable_voucher"`
EnableMultiBranch *bool `json:"enable_multi_branch"`
MaxBranches *int `json:"max_branches"`
EnableMultiKasir *bool `json:"enable_multi_kasir"`
MaxCashiers *int `json:"max_cashiers"`
EnforceSingleSession *bool `json:"enforce_single_session"`
}
func AdminUpdateCompanySettings(c *gin.Context) {
companyIDStr := c.Param("id")
companyID, err := strconv.Atoi(companyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID Perusahaan tidak valid"})
return
}
var req UpdateCompanySettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var company models.Company
if err := config.DB.First(&company, companyID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Perusahaan tidak ditemukan"})
return
}
if req.ChargeFeeBeforeDiscount != nil {
company.ChargeFeeBeforeDiscount = *req.ChargeFeeBeforeDiscount
}
if req.EnableMember != nil {
company.EnableMember = *req.EnableMember
}
if req.EnableVoucher != nil {
company.EnableVoucher = *req.EnableVoucher
}
if req.EnableMultiBranch != nil {
company.EnableMultiBranch = *req.EnableMultiBranch
}
if req.EnableMultiKasir != nil {
company.EnableMultiKasir = *req.EnableMultiKasir
}
if req.MaxBranches != nil {
company.MaxBranches = *req.MaxBranches
}
if req.MaxCashiers != nil {
company.MaxCashiers = *req.MaxCashiers
}
if req.EnforceSingleSession != nil {
company.EnforceSingleSession = *req.EnforceSingleSession
// Jika fitur dimatikan, hapus semua sesi aktif kasir perusahaan ini
if !*req.EnforceSingleSession {
var kasirIDs []uint
config.DB.Model(&models.User{}).Where("company_id = ? AND role = ?", company.ID, "kasir").Pluck("id", &kasirIDs)
if len(kasirIDs) > 0 {
config.DB.Where("user_id IN ?", kasirIDs).Delete(&models.UserSession{})
}
}
}
if err := config.DB.Save(&company).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan pengaturan"})
return
}
c.JSON(http.StatusOK, company)
}