Mhamdans17 commited on
Commit ·
10b6d0b
1
Parent(s): a90354c
feat: customizable fee structures per company from super admin panel
Browse files- .gitignore +1 -0
- controllers/fee_controller.go +40 -11
- controllers/order_controller.go +35 -2
- controllers/report_controller.go +20 -0
- models/models.go +7 -1
.gitignore
CHANGED
|
@@ -27,3 +27,4 @@ bin/
|
|
| 27 |
# OS files
|
| 28 |
.DS_Store
|
| 29 |
Thumbs.db
|
|
|
|
|
|
| 27 |
# OS files
|
| 28 |
.DS_Store
|
| 29 |
Thumbs.db
|
| 30 |
+
service-warungpos-go
|
controllers/fee_controller.go
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
package controllers
|
| 2 |
|
| 3 |
import (
|
|
|
|
| 4 |
"fmt"
|
| 5 |
"net/http"
|
| 6 |
"strconv"
|
|
@@ -205,6 +206,21 @@ func GetFeeReport(c *gin.Context) {
|
|
| 205 |
})
|
| 206 |
}
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
func SetCompanyFee(c *gin.Context) {
|
| 209 |
companyIDStr := c.Param("company_id")
|
| 210 |
companyID, err := strconv.Atoi(companyIDStr)
|
|
@@ -213,14 +229,13 @@ func SetCompanyFee(c *gin.Context) {
|
|
| 213 |
return
|
| 214 |
}
|
| 215 |
|
| 216 |
-
|
| 217 |
-
if
|
| 218 |
-
c.JSON(http.StatusBadRequest, gin.H{"detail": "
|
| 219 |
return
|
| 220 |
}
|
| 221 |
|
| 222 |
-
|
| 223 |
-
if err != nil || feePct < 0 || feePct > 100 {
|
| 224 |
c.JSON(http.StatusBadRequest, gin.H{"detail": "Fee percentage harus berupa angka antara 0 dan 100"})
|
| 225 |
return
|
| 226 |
}
|
|
@@ -231,16 +246,30 @@ func SetCompanyFee(c *gin.Context) {
|
|
| 231 |
return
|
| 232 |
}
|
| 233 |
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
if err := config.DB.Save(&company).Error; err != nil {
|
| 236 |
-
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan fee perusahaan"})
|
| 237 |
return
|
| 238 |
}
|
| 239 |
|
| 240 |
c.JSON(http.StatusOK, gin.H{
|
| 241 |
-
"message":
|
| 242 |
-
"company_id":
|
| 243 |
-
"company_name":
|
| 244 |
-
"fee_percentage": company.FeePercentage,
|
| 245 |
})
|
| 246 |
}
|
|
|
|
| 1 |
package controllers
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
"fmt"
|
| 6 |
"net/http"
|
| 7 |
"strconv"
|
|
|
|
| 206 |
})
|
| 207 |
}
|
| 208 |
|
| 209 |
+
type CashFeeRule struct {
|
| 210 |
+
MinAmount int `json:"min_amount"`
|
| 211 |
+
MaxAmount int `json:"max_amount"`
|
| 212 |
+
Fee int `json:"fee"`
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
type UpdateFeeRequest struct {
|
| 216 |
+
CashFeeRules []CashFeeRule `json:"cash_fee_rules"`
|
| 217 |
+
QrisFeePercentage float64 `json:"qris_fee_percentage"`
|
| 218 |
+
QrisFeeFixed int `json:"qris_fee_fixed"`
|
| 219 |
+
QrisFeeMin int `json:"qris_fee_min"`
|
| 220 |
+
QrisMinTransaction int `json:"qris_min_transaction"`
|
| 221 |
+
AllowQrisBelowMin bool `json:"allow_qris_below_min"`
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
func SetCompanyFee(c *gin.Context) {
|
| 225 |
companyIDStr := c.Param("company_id")
|
| 226 |
companyID, err := strconv.Atoi(companyIDStr)
|
|
|
|
| 229 |
return
|
| 230 |
}
|
| 231 |
|
| 232 |
+
var req UpdateFeeRequest
|
| 233 |
+
if err := c.ShouldBindJSON(&req); err != nil {
|
| 234 |
+
c.JSON(http.StatusBadRequest, gin.H{"detail": "Data konfigurasi fee tidak valid"})
|
| 235 |
return
|
| 236 |
}
|
| 237 |
|
| 238 |
+
if req.QrisFeePercentage < 0 || req.QrisFeePercentage > 100 {
|
|
|
|
| 239 |
c.JSON(http.StatusBadRequest, gin.H{"detail": "Fee percentage harus berupa angka antara 0 dan 100"})
|
| 240 |
return
|
| 241 |
}
|
|
|
|
| 246 |
return
|
| 247 |
}
|
| 248 |
|
| 249 |
+
rulesBytes, err := json.Marshal(req.CashFeeRules)
|
| 250 |
+
if err != nil {
|
| 251 |
+
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal memproses aturan fee tunai"})
|
| 252 |
+
return
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
company.CashFeeRules = string(rulesBytes)
|
| 256 |
+
company.QrisFeePercentage = req.QrisFeePercentage
|
| 257 |
+
company.QrisFeeFixed = req.QrisFeeFixed
|
| 258 |
+
company.QrisFeeMin = req.QrisFeeMin
|
| 259 |
+
company.QrisMinTransaction = req.QrisMinTransaction
|
| 260 |
+
company.AllowQrisBelowMin = req.AllowQrisBelowMin
|
| 261 |
+
|
| 262 |
+
// Keep backward compatibility if fee_percentage is still used somewhere
|
| 263 |
+
company.FeePercentage = req.QrisFeePercentage
|
| 264 |
+
|
| 265 |
if err := config.DB.Save(&company).Error; err != nil {
|
| 266 |
+
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan konfigurasi fee perusahaan"})
|
| 267 |
return
|
| 268 |
}
|
| 269 |
|
| 270 |
c.JSON(http.StatusOK, gin.H{
|
| 271 |
+
"message": fmt.Sprintf("Konfigurasi fee perusahaan '%s' berhasil diperbarui", company.Name),
|
| 272 |
+
"company_id": company.ID,
|
| 273 |
+
"company_name": company.Name,
|
|
|
|
| 274 |
})
|
| 275 |
}
|
controllers/order_controller.go
CHANGED
|
@@ -382,7 +382,25 @@ func PayCash(c *gin.Context) {
|
|
| 382 |
// Hitung fee platform berdasarkan Company
|
| 383 |
var company models.Company
|
| 384 |
if err := tx.First(&company, *user.CompanyID).Error; err == nil {
|
| 385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
}
|
| 387 |
|
| 388 |
if err := tx.Save(&order).Error; err != nil {
|
|
@@ -476,6 +494,17 @@ func PayTripay(c *gin.Context) {
|
|
| 476 |
return
|
| 477 |
}
|
| 478 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
// Load order items untuk dikirim ke Tripay
|
| 480 |
var orderItems []models.OrderItem
|
| 481 |
config.DB.Preload("Product").Where("order_id = ?", order.ID).Find(&orderItems)
|
|
@@ -626,7 +655,11 @@ func TripayWebhook(c *gin.Context) {
|
|
| 626 |
if order.PaymentStatus == "paid" && oldStatus != "paid" {
|
| 627 |
var company models.Company
|
| 628 |
if err := tx.First(&company, order.CompanyID).Error; err == nil {
|
| 629 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
awardPoints(tx, &order, &company)
|
| 631 |
}
|
| 632 |
}
|
|
|
|
| 382 |
// Hitung fee platform berdasarkan Company
|
| 383 |
var company models.Company
|
| 384 |
if err := tx.First(&company, *user.CompanyID).Error; err == nil {
|
| 385 |
+
fee := 0
|
| 386 |
+
if company.CashFeeRules != "" && company.CashFeeRules != "[]" {
|
| 387 |
+
var rules []map[string]interface{}
|
| 388 |
+
if err := json.Unmarshal([]byte(company.CashFeeRules), &rules); err == nil {
|
| 389 |
+
for _, r := range rules {
|
| 390 |
+
minAmt := int(r["min_amount"].(float64))
|
| 391 |
+
maxAmt := int(r["max_amount"].(float64))
|
| 392 |
+
if order.TotalAmount >= minAmt && order.TotalAmount <= maxAmt {
|
| 393 |
+
fee = int(r["fee"].(float64))
|
| 394 |
+
break
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
// Fallback ke fee lama jika tidak ada rule yang cocok/diset
|
| 400 |
+
if fee == 0 && company.FeePercentage > 0 {
|
| 401 |
+
fee = int(float64(order.TotalAmount) * company.FeePercentage / 100)
|
| 402 |
+
}
|
| 403 |
+
order.PlatformFee = fee
|
| 404 |
}
|
| 405 |
|
| 406 |
if err := tx.Save(&order).Error; err != nil {
|
|
|
|
| 494 |
return
|
| 495 |
}
|
| 496 |
|
| 497 |
+
var company models.Company
|
| 498 |
+
if err := config.DB.First(&company, *user.CompanyID).Error; err != nil {
|
| 499 |
+
c.JSON(http.StatusNotFound, gin.H{"detail": "Perusahaan tidak ditemukan"})
|
| 500 |
+
return
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
if order.TotalAmount < company.QrisMinTransaction && !company.AllowQrisBelowMin {
|
| 504 |
+
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("Minimal transaksi QRIS adalah Rp%s. Silakan hubungi admin untuk mengizinkan transaksi kecil.", fmtRupiah(company.QrisMinTransaction))})
|
| 505 |
+
return
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
// Load order items untuk dikirim ke Tripay
|
| 509 |
var orderItems []models.OrderItem
|
| 510 |
config.DB.Preload("Product").Where("order_id = ?", order.ID).Find(&orderItems)
|
|
|
|
| 655 |
if order.PaymentStatus == "paid" && oldStatus != "paid" {
|
| 656 |
var company models.Company
|
| 657 |
if err := tx.First(&company, order.CompanyID).Error; err == nil {
|
| 658 |
+
fee := int((float64(order.TotalAmount) * company.QrisFeePercentage / 100) + float64(company.QrisFeeFixed))
|
| 659 |
+
if fee < company.QrisFeeMin {
|
| 660 |
+
fee = company.QrisFeeMin
|
| 661 |
+
}
|
| 662 |
+
order.PlatformFee = fee
|
| 663 |
awardPoints(tx, &order, &company)
|
| 664 |
}
|
| 665 |
}
|
controllers/report_controller.go
CHANGED
|
@@ -239,6 +239,21 @@ func SummaryReport(c *gin.Context) {
|
|
| 239 |
todayQuery.Count(&todayOrders)
|
| 240 |
todayQuery.Select("coalesce(sum(total_amount), 0)").Row().Scan(&todayRevenue)
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
c.JSON(http.StatusOK, gin.H{
|
| 243 |
"all_time": gin.H{
|
| 244 |
"total_orders": totalOrders,
|
|
@@ -248,6 +263,11 @@ func SummaryReport(c *gin.Context) {
|
|
| 248 |
"total_orders": todayOrders,
|
| 249 |
"total_revenue": todayRevenue,
|
| 250 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
"total_products": totalProducts,
|
| 252 |
"top_products": topProducts,
|
| 253 |
})
|
|
|
|
| 239 |
todayQuery.Count(&todayOrders)
|
| 240 |
todayQuery.Select("coalesce(sum(total_amount), 0)").Row().Scan(&todayRevenue)
|
| 241 |
|
| 242 |
+
// 5. Laporan QRIS
|
| 243 |
+
var qrisRevenue int64
|
| 244 |
+
var qrisFee int64
|
| 245 |
+
|
| 246 |
+
qrisQuery := config.DB.Model(&models.Order{}).
|
| 247 |
+
Where("company_id = ? AND payment_status = ? AND payment_method != 'cash'", *user.CompanyID, "paid")
|
| 248 |
+
|
| 249 |
+
if branchQuery != "" {
|
| 250 |
+
if bID, err := strconv.Atoi(branchQuery); err == nil {
|
| 251 |
+
qrisQuery = qrisQuery.Where("branch_id = ?", bID)
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
qrisQuery.Select("coalesce(sum(total_amount), 0), coalesce(sum(platform_fee), 0)").Row().Scan(&qrisRevenue, &qrisFee)
|
| 256 |
+
|
| 257 |
c.JSON(http.StatusOK, gin.H{
|
| 258 |
"all_time": gin.H{
|
| 259 |
"total_orders": totalOrders,
|
|
|
|
| 263 |
"total_orders": todayOrders,
|
| 264 |
"total_revenue": todayRevenue,
|
| 265 |
},
|
| 266 |
+
"qris_report": gin.H{
|
| 267 |
+
"total_transactions": qrisRevenue,
|
| 268 |
+
"system_fee": qrisFee,
|
| 269 |
+
"net_amount": qrisRevenue - qrisFee,
|
| 270 |
+
},
|
| 271 |
"total_products": totalProducts,
|
| 272 |
"top_products": topProducts,
|
| 273 |
})
|
models/models.go
CHANGED
|
@@ -20,7 +20,13 @@ type Company struct {
|
|
| 20 |
TripayAPIKey string `gorm:"type:text" json:"tripay_api_key"`
|
| 21 |
TripayPrivateKey string `gorm:"type:text" json:"tripay_private_key"`
|
| 22 |
TripayProxyURL string `gorm:"type:text" json:"tripay_proxy_url"`
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
| 25 |
Branches []Branch `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"-"`
|
| 26 |
Users []User `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"-"`
|
|
|
|
| 20 |
TripayAPIKey string `gorm:"type:text" json:"tripay_api_key"`
|
| 21 |
TripayPrivateKey string `gorm:"type:text" json:"tripay_private_key"`
|
| 22 |
TripayProxyURL string `gorm:"type:text" json:"tripay_proxy_url"`
|
| 23 |
+
CashFeeRules string `gorm:"type:text;default:'[]'" json:"cash_fee_rules"`
|
| 24 |
+
QrisFeePercentage float64 `gorm:"type:float;default:1.7" json:"qris_fee_percentage"`
|
| 25 |
+
QrisFeeFixed int `gorm:"default:500" json:"qris_fee_fixed"`
|
| 26 |
+
QrisFeeMin int `gorm:"default:850" json:"qris_fee_min"`
|
| 27 |
+
QrisMinTransaction int `gorm:"default:10000" json:"qris_min_transaction"`
|
| 28 |
+
AllowQrisBelowMin bool `gorm:"default:false" json:"allow_qris_below_min"`
|
| 29 |
+
PointRules []PointRule `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"point_rules"`
|
| 30 |
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
| 31 |
Branches []Branch `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"-"`
|
| 32 |
Users []User `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"-"`
|