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

feat: backend for deposit wallet, fee deduction, and super admin manual adjust testing

Browse files
controllers/order_controller.go CHANGED
@@ -349,6 +349,20 @@ func PayCash(c *gin.Context) {
349
  return
350
  }
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  if order.PaymentStatus == "paid" {
353
  tx.Rollback()
354
  c.JSON(http.StatusBadRequest, gin.H{"detail": "Order sudah dibayar"})
@@ -380,27 +394,47 @@ func PayCash(c *gin.Context) {
380
  }
381
 
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 {
 
349
  return
350
  }
351
 
352
+ var company models.Company
353
+ if err := tx.First(&company, *user.CompanyID).Error; err != nil {
354
+ tx.Rollback()
355
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Company tidak ditemukan"})
356
+ return
357
+ }
358
+
359
+ // 1. Cek Limit Dompet Mengendap
360
+ if company.WalletBalance <= company.WalletLimit {
361
+ tx.Rollback()
362
+ c.JSON(http.StatusPaymentRequired, gin.H{"detail": "Saldo limit dompet minus. Hubungi Owner untuk melakukan Top-up saldo sistem terlebih dahulu agar bisa menerima pembayaran tunai."})
363
+ return
364
+ }
365
+
366
  if order.PaymentStatus == "paid" {
367
  tx.Rollback()
368
  c.JSON(http.StatusBadRequest, gin.H{"detail": "Order sudah dibayar"})
 
394
  }
395
 
396
  // Hitung fee platform berdasarkan Company
397
+ fee := 0
398
+ if company.CashFeeRules != "" && company.CashFeeRules != "[]" {
399
+ var rules []map[string]interface{}
400
+ if err := json.Unmarshal([]byte(company.CashFeeRules), &rules); err == nil {
401
+ for _, r := range rules {
402
+ minAmt := int(r["min_amount"].(float64))
403
+ maxAmt := int(r["max_amount"].(float64))
404
+ if order.TotalAmount >= minAmt && order.TotalAmount <= maxAmt {
405
+ fee = int(r["fee"].(float64))
406
+ break
 
 
 
407
  }
408
  }
409
  }
410
+ }
411
+ // Fallback ke fee lama jika tidak ada rule yang cocok/diset
412
+ if fee == 0 && company.FeePercentage > 0 {
413
+ fee = int(float64(order.TotalAmount) * company.FeePercentage / 100)
414
+ }
415
+ order.PlatformFee = fee
416
+
417
+ // 2. Potong saldo dompet jika ada fee
418
+ if fee > 0 {
419
+ company.WalletBalance -= float64(fee)
420
+ if err := tx.Save(&company).Error; err != nil {
421
+ tx.Rollback()
422
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal memotong saldo dompet"})
423
+ return
424
+ }
425
+
426
+ walletTx := models.WalletTransaction{
427
+ CompanyID: company.ID,
428
+ Amount: -float64(fee),
429
+ Type: "cash_fee",
430
+ RefID: order.OrderNumber,
431
+ Desc: "Potongan fee transaksi tunai",
432
+ }
433
+ if err := tx.Create(&walletTx).Error; err != nil {
434
+ tx.Rollback()
435
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mencatat mutasi dompet"})
436
+ return
437
  }
 
438
  }
439
 
440
  if err := tx.Save(&order).Error; err != nil {
controllers/wallet_controller.go ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controllers
2
+
3
+ import (
4
+ "fmt"
5
+ "net/http"
6
+ "service-warungpos-go/config"
7
+ "service-warungpos-go/models"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ // ==================== MERCHANT ENDPOINTS ====================
13
+
14
+ // GetWalletDetail mengembalikan saldo dompet dan riwayat mutasi perusahaan
15
+ func GetWalletDetail(c *gin.Context) {
16
+ userVal, _ := c.Get("user")
17
+ user := userVal.(*models.User)
18
+
19
+ var company models.Company
20
+ if err := config.DB.First(&company, *user.CompanyID).Error; err != nil {
21
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Company tidak ditemukan"})
22
+ return
23
+ }
24
+
25
+ var history []models.WalletTransaction
26
+ if err := config.DB.Where("company_id = ?", company.ID).Order("created_at desc").Limit(50).Find(&history).Error; err != nil {
27
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil riwayat dompet"})
28
+ return
29
+ }
30
+
31
+ c.JSON(http.StatusOK, gin.H{
32
+ "balance": company.WalletBalance,
33
+ "limit": company.WalletLimit,
34
+ "history": history,
35
+ })
36
+ }
37
+
38
+ type TopupManualRequest struct {
39
+ Amount float64 `json:"amount" binding:"required,gt=0"`
40
+ ProofURL string `json:"proof_url" binding:"required"`
41
+ }
42
+
43
+ // RequestTopupManual mengajukan penambahan saldo (menunggu persetujuan admin)
44
+ func RequestTopupManual(c *gin.Context) {
45
+ userVal, _ := c.Get("user")
46
+ user := userVal.(*models.User)
47
+
48
+ var req TopupManualRequest
49
+ if err := c.ShouldBindJSON(&req); err != nil {
50
+ c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
51
+ return
52
+ }
53
+
54
+ topup := models.TopupRequest{
55
+ CompanyID: *user.CompanyID,
56
+ Amount: req.Amount,
57
+ ProofURL: req.ProofURL,
58
+ Status: "pending",
59
+ }
60
+
61
+ if err := config.DB.Create(&topup).Error; err != nil {
62
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengajukan top-up"})
63
+ return
64
+ }
65
+
66
+ c.JSON(http.StatusCreated, gin.H{"message": "Pengajuan top-up berhasil dibuat, menunggu persetujuan admin", "data": topup})
67
+ }
68
+
69
+ // ==================== SUPER ADMIN ENDPOINTS ====================
70
+
71
+ // AdminGetPendingTopups melihat semua pengajuan topup yang menunggu
72
+ func AdminGetPendingTopups(c *gin.Context) {
73
+ var topups []models.TopupRequest
74
+ if err := config.DB.Preload("Company").Where("status = ?", "pending").Order("created_at asc").Find(&topups).Error; err != nil {
75
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data top-up"})
76
+ return
77
+ }
78
+ c.JSON(http.StatusOK, topups)
79
+ }
80
+
81
+ // AdminApproveTopup menyetujui pengajuan topup dan menambah saldo dompet
82
+ func AdminApproveTopup(c *gin.Context) {
83
+ topupID := c.Param("id")
84
+
85
+ tx := config.DB.Begin()
86
+ defer func() {
87
+ if r := recover(); r != nil {
88
+ tx.Rollback()
89
+ }
90
+ }()
91
+
92
+ var topup models.TopupRequest
93
+ if err := tx.First(&topup, topupID).Error; err != nil {
94
+ tx.Rollback()
95
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Request Top-up tidak ditemukan"})
96
+ return
97
+ }
98
+
99
+ if topup.Status != "pending" {
100
+ tx.Rollback()
101
+ c.JSON(http.StatusBadRequest, gin.H{"detail": "Status top-up sudah diproses sebelumnya"})
102
+ return
103
+ }
104
+
105
+ topup.Status = "approved"
106
+ if err := tx.Save(&topup).Error; err != nil {
107
+ tx.Rollback()
108
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal update status topup"})
109
+ return
110
+ }
111
+
112
+ // Tambahkan saldo ke perusahaan
113
+ var company models.Company
114
+ if err := tx.First(&company, topup.CompanyID).Error; err != nil {
115
+ tx.Rollback()
116
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Company tidak ditemukan"})
117
+ return
118
+ }
119
+
120
+ company.WalletBalance += topup.Amount
121
+ if err := tx.Save(&company).Error; err != nil {
122
+ tx.Rollback()
123
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal update saldo perusahaan"})
124
+ return
125
+ }
126
+
127
+ // Catat di log transaksi dompet
128
+ walletTx := models.WalletTransaction{
129
+ CompanyID: company.ID,
130
+ Amount: topup.Amount,
131
+ Type: "topup_manual",
132
+ RefID: fmt.Sprintf("TOPUP-%d", topup.ID),
133
+ Desc: "Top-up Saldo Manual (Disetujui Admin)",
134
+ }
135
+ if err := tx.Create(&walletTx).Error; err != nil {
136
+ tx.Rollback()
137
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mencatat log dompet"})
138
+ return
139
+ }
140
+
141
+ if err := tx.Commit().Error; err != nil {
142
+ tx.Rollback()
143
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan perubahan"})
144
+ return
145
+ }
146
+
147
+ c.JSON(http.StatusOK, gin.H{"message": "Top-up berhasil disetujui", "new_balance": company.WalletBalance})
148
+ }
149
+
150
+ // AdminRejectTopup menolak pengajuan topup
151
+ func AdminRejectTopup(c *gin.Context) {
152
+ topupID := c.Param("id")
153
+
154
+ var topup models.TopupRequest
155
+ if err := config.DB.First(&topup, topupID).Error; err != nil {
156
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Request Top-up tidak ditemukan"})
157
+ return
158
+ }
159
+
160
+ if topup.Status != "pending" {
161
+ c.JSON(http.StatusBadRequest, gin.H{"detail": "Status top-up sudah diproses sebelumnya"})
162
+ return
163
+ }
164
+
165
+ topup.Status = "rejected"
166
+ if err := config.DB.Save(&topup).Error; err != nil {
167
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menolak topup"})
168
+ return
169
+ }
170
+
171
+ c.JSON(http.StatusOK, gin.H{"message": "Top-up berhasil ditolak"})
172
+ }
173
+
174
+ type AdminAdjustWalletRequest struct {
175
+ Amount float64 `json:"amount" binding:"required"`
176
+ }
177
+
178
+ // AdminAdjustWallet (Untuk keperluan testing) menyesuaikan saldo tanpa topup
179
+ func AdminAdjustWallet(c *gin.Context) {
180
+ companyID := c.Param("id")
181
+
182
+ var req AdminAdjustWalletRequest
183
+ if err := c.ShouldBindJSON(&req); err != nil {
184
+ c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
185
+ return
186
+ }
187
+
188
+ tx := config.DB.Begin()
189
+ defer func() {
190
+ if r := recover(); r != nil {
191
+ tx.Rollback()
192
+ }
193
+ }()
194
+
195
+ var company models.Company
196
+ if err := tx.First(&company, companyID).Error; err != nil {
197
+ tx.Rollback()
198
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Company tidak ditemukan"})
199
+ return
200
+ }
201
+
202
+ company.WalletBalance += req.Amount
203
+ if err := tx.Save(&company).Error; err != nil {
204
+ tx.Rollback()
205
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal update saldo perusahaan"})
206
+ return
207
+ }
208
+
209
+ walletTx := models.WalletTransaction{
210
+ CompanyID: company.ID,
211
+ Amount: req.Amount,
212
+ Type: "admin_adjustment",
213
+ RefID: "ADMIN",
214
+ Desc: "Penyesuaian saldo manual oleh admin",
215
+ }
216
+ if err := tx.Create(&walletTx).Error; err != nil {
217
+ tx.Rollback()
218
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mencatat log dompet"})
219
+ return
220
+ }
221
+
222
+ if err := tx.Commit().Error; err != nil {
223
+ tx.Rollback()
224
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan perubahan"})
225
+ return
226
+ }
227
+
228
+ c.JSON(http.StatusOK, gin.H{"message": "Saldo berhasil disesuaikan", "new_balance": company.WalletBalance})
229
+ }
main.go CHANGED
@@ -37,6 +37,8 @@ func main() {
37
  &models.Setting{},
38
  &models.Member{},
39
  &models.PointRule{},
 
 
40
  )
41
  if err != nil {
42
  log.Fatalf("Gagal melakukan Auto-Migration: %v", err)
@@ -234,6 +236,35 @@ func main() {
234
  fees.PUT("/companies/:company_id/set-fee", controllers.SetCompanyFee)
235
  }
236
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  }
238
 
239
  // 7. Mulai Server (Membaca dinamis port Hugging Face Spaces jika ada)
 
37
  &models.Setting{},
38
  &models.Member{},
39
  &models.PointRule{},
40
+ &models.WalletTransaction{},
41
+ &models.TopupRequest{},
42
  )
43
  if err != nil {
44
  log.Fatalf("Gagal melakukan Auto-Migration: %v", err)
 
236
  fees.PUT("/companies/:company_id/set-fee", controllers.SetCompanyFee)
237
  }
238
  }
239
+
240
+ // === WALLET ROUTES ===
241
+ wallet := api.Group("/wallet")
242
+ {
243
+ wallet.Use(middleware.GetCurrentUser(), middleware.RequireTenantContext())
244
+ {
245
+ wallet.GET("/", controllers.GetWalletDetail)
246
+ wallet.POST("/topup/manual", controllers.RequestTopupManual)
247
+ }
248
+ }
249
+
250
+ // === ADMIN TOPUP & WALLET ROUTES ===
251
+ adminTopups := api.Group("/admin/topups")
252
+ {
253
+ adminTopups.Use(middleware.GetCurrentUser(), middleware.RequireSuperAdmin())
254
+ {
255
+ adminTopups.GET("/", controllers.AdminGetPendingTopups)
256
+ adminTopups.POST("/:id/approve", controllers.AdminApproveTopup)
257
+ adminTopups.POST("/:id/reject", controllers.AdminRejectTopup)
258
+ }
259
+ }
260
+
261
+ adminCompanies := api.Group("/admin/companies")
262
+ {
263
+ adminCompanies.Use(middleware.GetCurrentUser(), middleware.RequireSuperAdmin())
264
+ {
265
+ adminCompanies.POST("/:id/adjust-wallet", controllers.AdminAdjustWallet)
266
+ }
267
+ }
268
  }
269
 
270
  // 7. Mulai Server (Membaca dinamis port Hugging Face Spaces jika ada)
models/models.go CHANGED
@@ -26,6 +26,8 @@ type Company struct {
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:"-"`
@@ -175,3 +177,25 @@ type Setting struct {
175
  TripayPaymentMethod string `gorm:"type:varchar(50);default:'QRIS'" json:"tripay_payment_method"`
176
  UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
177
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ WalletBalance float64 `gorm:"type:float;default:0.0" json:"wallet_balance"`
30
+ WalletLimit float64 `gorm:"type:float;default:-50000.0" json:"wallet_limit"`
31
  PointRules []PointRule `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"point_rules"`
32
  CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
33
  Branches []Branch `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"-"`
 
177
  TripayPaymentMethod string `gorm:"type:varchar(50);default:'QRIS'" json:"tripay_payment_method"`
178
  UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
179
  }
180
+
181
+ type WalletTransaction struct {
182
+ ID uint `gorm:"primaryKey" json:"id"`
183
+ CompanyID uint `gorm:"not null;index" json:"company_id"`
184
+ Amount float64 `gorm:"not null" json:"amount"`
185
+ Type string `gorm:"type:varchar(50);not null" json:"type"` // "cash_fee", "topup_qris", "topup_manual", "admin_adjustment"
186
+ RefID string `gorm:"type:varchar(100)" json:"ref_id"`
187
+ Desc string `gorm:"type:varchar(255)" json:"desc"`
188
+ CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
189
+ Company *Company `gorm:"foreignKey:CompanyID" json:"company,omitempty"`
190
+ }
191
+
192
+ type TopupRequest struct {
193
+ ID uint `gorm:"primaryKey" json:"id"`
194
+ CompanyID uint `gorm:"not null;index" json:"company_id"`
195
+ Amount float64 `gorm:"not null" json:"amount"`
196
+ ProofURL string `gorm:"type:varchar(255);not null" json:"proof_url"`
197
+ Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // pending, approved, rejected
198
+ CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
199
+ UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
200
+ Company *Company `gorm:"foreignKey:CompanyID" json:"company,omitempty"`
201
+ }