Mhamdans17
feat: add max branches and cashiers limit, refine admin UI settings, implement tier-based qris fee
8879e0b | 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, | |
| }) | |
| } | |