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, }) }