Dev / controllers /fee_controller.go
Mhamdans17
feat: add max branches and cashiers limit, refine admin UI settings, implement tier-based qris fee
8879e0b
Raw
History Blame Contribute Delete
10.1 kB
package controllers
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"service-warungpos-go/config"
"service-warungpos-go/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type FeeSummaryData struct {
TotalTransactions int64 `json:"total_transactions"`
TotalRevenue int `json:"total_revenue"`
TotalFee int `json:"total_fee"`
TotalCash int `json:"total_cash"`
TotalMidtrans int `json:"total_midtrans"`
FeeCash int `json:"fee_cash"`
FeeMidtrans int `json:"fee_midtrans"`
}
type CompanyFeeRow struct {
CompanyID uint `json:"company_id"`
CompanyName string `json:"company_name"`
FeePercentage float64 `json:"fee_percentage"`
TotalTransactions int64 `json:"total_transactions"`
TotalRevenue int `json:"total_revenue"`
TotalFee int `json:"total_fee"`
TotalCash int `json:"total_cash"`
TotalMidtrans int `json:"total_midtrans"`
}
type BranchFeeRow struct {
CompanyID uint `json:"company_id"`
CompanyName string `json:"company_name"`
BranchID *uint `json:"branch_id"`
BranchName string `json:"branch_name"`
TotalTransactions int64 `json:"total_transactions"`
TotalRevenue int `json:"total_revenue"`
TotalFee int `json:"total_fee"`
TotalCash int `json:"total_cash"`
TotalMidtrans int `json:"total_midtrans"`
}
type DailyFeeRow struct {
Day time.Time `json:"day"`
CompanyID uint `json:"company_id"`
CompanyName string `json:"company_name"`
FeePercentage float64 `json:"fee_percentage"`
TotalTransactions int64 `json:"total_transactions"`
TotalRevenue int `json:"total_revenue"`
TotalFee int `json:"total_fee"`
}
func GetFeeReport(c *gin.Context) {
dateFrom := c.Query("date_from")
dateTo := c.Query("date_to")
companyIDStr := c.Query("company_id")
today := time.Now()
var dFrom, dTo time.Time
var err error
if dateFrom != "" {
dFrom, err = time.Parse("2006-01-02", dateFrom)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Format date_from salah, gunakan YYYY-MM-DD"})
return
}
} else {
// Default 30 hari lalu
dFrom = today.AddDate(0, 0, -29)
}
if dateTo != "" {
dTo, err = time.Parse("2006-01-02", dateTo)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Format date_to salah, gunakan YYYY-MM-DD"})
return
}
} else {
dTo = today
}
loc, _ := time.LoadLocation("Asia/Jakarta")
if loc == nil {
loc = time.Local
}
startDt := time.Date(dFrom.Year(), dFrom.Month(), dFrom.Day(), 0, 0, 0, 0, loc)
endDt := time.Date(dTo.Year(), dTo.Month(), dTo.Day(), 23, 59, 59, 0, loc)
// Helper local untuk membikin query fresh guna menghindari GORM method chaining state sharing bug (table name specified more than once)
getFreshFeeQuery := func() *gorm.DB {
q := config.DB.Table("orders").
Where("orders.payment_status = ? AND orders.created_at >= ? AND orders.created_at <= ?", "paid", startDt, endDt)
if companyIDStr != "" {
if cID, err := strconv.Atoi(companyIDStr); err == nil {
q = q.Where("orders.company_id = ?", cID)
}
}
return q
}
// 1. Summary keseluruhan
var summary FeeSummaryData
getFreshFeeQuery().Select(`
count(orders.id) as total_transactions,
coalesce(sum(orders.total_amount), 0) as total_revenue,
coalesce(sum(orders.platform_fee), 0) as total_fee,
coalesce(sum(case when orders.payment_method = 'cash' then orders.total_amount else 0 end), 0) as total_cash,
coalesce(sum(case when orders.payment_method != 'cash' then orders.total_amount else 0 end), 0) as total_midtrans,
coalesce(sum(case when orders.payment_method = 'cash' then orders.platform_fee else 0 end), 0) as fee_cash,
coalesce(sum(case when orders.payment_method != 'cash' then orders.platform_fee else 0 end), 0) as fee_midtrans
`).Scan(&summary)
// 2. Summary per perusahaan
var perCompany []CompanyFeeRow
getFreshFeeQuery().Select(`
orders.company_id,
companies.name as company_name,
companies.fee_percentage,
count(orders.id) as total_transactions,
coalesce(sum(orders.total_amount), 0) as total_revenue,
coalesce(sum(orders.platform_fee), 0) as total_fee,
coalesce(sum(case when orders.payment_method = 'cash' then orders.total_amount else 0 end), 0) as total_cash,
coalesce(sum(case when orders.payment_method != 'cash' then orders.total_amount else 0 end), 0) as total_midtrans
`).
Joins("join companies on companies.id = orders.company_id").
Group("orders.company_id, companies.name, companies.fee_percentage").
Order("total_fee desc").
Scan(&perCompany)
// 3. Summary per cabang
var perBranch []BranchFeeRow
getFreshFeeQuery().Select(`
orders.company_id,
companies.name as company_name,
orders.branch_id,
coalesce(branches.name, 'Tanpa Cabang') as branch_name,
count(orders.id) as total_transactions,
coalesce(sum(orders.total_amount), 0) as total_revenue,
coalesce(sum(orders.platform_fee), 0) as total_fee,
coalesce(sum(case when orders.payment_method = 'cash' then orders.total_amount else 0 end), 0) as total_cash,
coalesce(sum(case when orders.payment_method != 'cash' then orders.total_amount else 0 end), 0) as total_midtrans
`).
Joins("join companies on companies.id = orders.company_id").
Joins("left join branches on branches.id = orders.branch_id").
Group("orders.company_id, companies.name, orders.branch_id, branches.name").
Order("orders.company_id, total_revenue desc").
Scan(&perBranch)
// 4. Daily breakdown
var dailyBreakdown []DailyFeeRow
getFreshFeeQuery().Select(`
DATE(orders.created_at) as day,
orders.company_id,
companies.name as company_name,
companies.fee_percentage,
count(orders.id) as total_transactions,
coalesce(sum(orders.total_amount), 0) as total_revenue,
coalesce(sum(orders.platform_fee), 0) as total_fee
`).
Joins("join companies on companies.id = orders.company_id").
Group("DATE(orders.created_at), orders.company_id, companies.name, companies.fee_percentage").
Order("day desc").
Scan(&dailyBreakdown)
// Buat daily_breakdown response yang clean format tanggalnya
type DailyBreakdownResp struct {
Date string `json:"date"`
CompanyID uint `json:"company_id"`
CompanyName string `json:"company_name"`
FeePercentage float64 `json:"fee_percentage"`
TotalTransactions int64 `json:"total_transactions"`
TotalRevenue int `json:"total_revenue"`
TotalFee int `json:"total_fee"`
}
var dailyResp []DailyBreakdownResp
for _, row := range dailyBreakdown {
dailyResp = append(dailyResp, DailyBreakdownResp{
Date: row.Day.Format("2006-01-02"),
CompanyID: row.CompanyID,
CompanyName: row.CompanyName,
FeePercentage: row.FeePercentage,
TotalTransactions: row.TotalTransactions,
TotalRevenue: row.TotalRevenue,
TotalFee: row.TotalFee,
})
}
c.JSON(http.StatusOK, gin.H{
"period": gin.H{
"date_from": dFrom.Format("2006-01-02"),
"date_to": dTo.Format("2006-01-02"),
},
"summary": summary,
"per_company": perCompany,
"per_branch": perBranch,
"daily_breakdown": dailyResp,
})
}
type CashFeeRule struct {
MinAmount int `json:"min_amount"`
MaxAmount int `json:"max_amount"`
Fee int `json:"fee"`
}
type QrisFeeRule struct {
MinAmount int `json:"min_amount"`
MaxAmount int `json:"max_amount"`
FeePercentage float64 `json:"fee_percentage"`
FeeFixed int `json:"fee_fixed"`
FeeMin int `json:"fee_min"`
}
type UpdateFeeRequest struct {
CashFeeRules []CashFeeRule `json:"cash_fee_rules"`
QrisFeeRules []QrisFeeRule `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"`
}
func SetCompanyFee(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 UpdateFeeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Data konfigurasi fee tidak valid"})
return
}
if req.QrisFeePercentage < 0 || req.QrisFeePercentage > 100 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Fee percentage harus berupa angka antara 0 dan 100"})
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
}
rulesBytes, err := json.Marshal(req.CashFeeRules)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal memproses aturan fee tunai"})
return
}
qrisRulesBytes, err := json.Marshal(req.QrisFeeRules)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal memproses aturan fee qris"})
return
}
company.CashFeeRules = string(rulesBytes)
company.QrisFeeRules = string(qrisRulesBytes)
company.QrisFeePercentage = req.QrisFeePercentage
company.QrisFeeFixed = req.QrisFeeFixed
company.QrisFeeMin = req.QrisFeeMin
company.QrisMinTransaction = req.QrisMinTransaction
company.AllowQrisBelowMin = req.AllowQrisBelowMin
// Keep backward compatibility if fee_percentage is still used somewhere
company.FeePercentage = req.QrisFeePercentage
if err := config.DB.Save(&company).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan konfigurasi fee perusahaan"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Konfigurasi fee perusahaan '%s' berhasil diperbarui", company.Name),
"company_id": company.ID,
"company_name": company.Name,
})
}