Mhamdans17 commited on
Commit
10b6d0b
·
1 Parent(s): a90354c

feat: customizable fee structures per company from super admin panel

Browse files
.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
- feePctStr := c.Query("fee_percentage")
217
- if feePctStr == "" {
218
- c.JSON(http.StatusBadRequest, gin.H{"detail": "Query parameter fee_percentage wajib diisi"})
219
  return
220
  }
221
 
222
- feePct, err := strconv.ParseFloat(feePctStr, 64)
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
- company.FeePercentage = feePct
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": fmt.Sprintf("Fee perusahaan '%s' berhasil diatur ke %.2f%%", company.Name, company.FeePercentage),
242
- "company_id": company.ID,
243
- "company_name": 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
- order.PlatformFee = int(float64(order.TotalAmount) * company.FeePercentage / 100)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- order.PlatformFee = int(float64(order.TotalAmount) * company.FeePercentage / 100)
 
 
 
 
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
- PointRules []PointRule `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"point_rules"`
 
 
 
 
 
 
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:"-"`