Mhamdans17 commited on
Commit
7121ebc
·
1 Parent(s): 7b31d21

Update WhatsApp notification format, add voucher to order reports, and add tenant settings toggle

Browse files
.gitignore CHANGED
@@ -28,3 +28,5 @@ bin/
28
  .DS_Store
29
  Thumbs.db
30
  service-warungpos-go
 
 
 
28
  .DS_Store
29
  Thumbs.db
30
  service-warungpos-go
31
+ main
32
+ tmp_build
controllers/company_controller.go CHANGED
@@ -31,6 +31,7 @@ type CompanyResponse struct {
31
  QrisFeeMin int `json:"qris_fee_min"`
32
  QrisMinTransaction int `json:"qris_min_transaction"`
33
  AllowQrisBelowMin bool `json:"allow_qris_below_min"`
 
34
  WalletBalance float64 `json:"wallet_balance"`
35
  WalletLimit float64 `json:"wallet_limit"`
36
  ApprovedAt *time.Time `json:"approved_at"`
@@ -100,7 +101,7 @@ func ListCompanies(c *gin.Context) {
100
  searchQuery := c.Query("search")
101
 
102
  query := config.DB.Model(&models.Company{}).
103
- Select("id, name, bank_account_number, bank_name, address, email, phone, logo_url, is_active, fee_percentage, cash_fee_rules, qris_fee_percentage, qris_fee_fixed, qris_fee_min, qris_min_transaction, allow_qris_below_min, wallet_balance, wallet_limit, approved_at, created_at")
104
 
105
  if searchQuery != "" {
106
  query = query.Where("name ILIKE ? OR email ILIKE ? OR phone ILIKE ? OR address ILIKE ?",
@@ -206,8 +207,9 @@ func ListCompanies(c *gin.Context) {
206
  QrisFeeFixed: comp.QrisFeeFixed,
207
  QrisFeeMin: comp.QrisFeeMin,
208
  QrisMinTransaction: comp.QrisMinTransaction,
209
- AllowQrisBelowMin: comp.AllowQrisBelowMin,
210
- WalletBalance: comp.WalletBalance,
 
211
  WalletLimit: comp.WalletLimit,
212
  CreatedAt: comp.CreatedAt,
213
  })
@@ -932,3 +934,39 @@ func ToggleProductAvailability(c *gin.Context) {
932
  "is_available": bp.IsAvailable,
933
  })
934
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  QrisFeeMin int `json:"qris_fee_min"`
32
  QrisMinTransaction int `json:"qris_min_transaction"`
33
  AllowQrisBelowMin bool `json:"allow_qris_below_min"`
34
+ ChargeFeeBeforeDiscount bool `json:"charge_fee_before_discount"`
35
  WalletBalance float64 `json:"wallet_balance"`
36
  WalletLimit float64 `json:"wallet_limit"`
37
  ApprovedAt *time.Time `json:"approved_at"`
 
101
  searchQuery := c.Query("search")
102
 
103
  query := config.DB.Model(&models.Company{}).
104
+ Select("id, name, bank_account_number, bank_name, address, email, phone, logo_url, is_active, fee_percentage, cash_fee_rules, qris_fee_percentage, qris_fee_fixed, qris_fee_min, qris_min_transaction, allow_qris_below_min, charge_fee_before_discount, wallet_balance, wallet_limit, approved_at, created_at")
105
 
106
  if searchQuery != "" {
107
  query = query.Where("name ILIKE ? OR email ILIKE ? OR phone ILIKE ? OR address ILIKE ?",
 
207
  QrisFeeFixed: comp.QrisFeeFixed,
208
  QrisFeeMin: comp.QrisFeeMin,
209
  QrisMinTransaction: comp.QrisMinTransaction,
210
+ AllowQrisBelowMin: comp.AllowQrisBelowMin,
211
+ ChargeFeeBeforeDiscount: comp.ChargeFeeBeforeDiscount,
212
+ WalletBalance: comp.WalletBalance,
213
  WalletLimit: comp.WalletLimit,
214
  CreatedAt: comp.CreatedAt,
215
  })
 
934
  "is_available": bp.IsAvailable,
935
  })
936
  }
937
+
938
+ type UpdateCompanySettingsRequest struct {
939
+ ChargeFeeBeforeDiscount *bool `json:"charge_fee_before_discount"`
940
+ }
941
+
942
+ func AdminUpdateCompanySettings(c *gin.Context) {
943
+ companyIDStr := c.Param("id")
944
+ companyID, err := strconv.Atoi(companyIDStr)
945
+ if err != nil {
946
+ c.JSON(http.StatusBadRequest, gin.H{"detail": "ID Perusahaan tidak valid"})
947
+ return
948
+ }
949
+
950
+ var req UpdateCompanySettingsRequest
951
+ if err := c.ShouldBindJSON(&req); err != nil {
952
+ c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
953
+ return
954
+ }
955
+
956
+ var company models.Company
957
+ if err := config.DB.First(&company, companyID).Error; err != nil {
958
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Perusahaan tidak ditemukan"})
959
+ return
960
+ }
961
+
962
+ if req.ChargeFeeBeforeDiscount != nil {
963
+ company.ChargeFeeBeforeDiscount = *req.ChargeFeeBeforeDiscount
964
+ }
965
+
966
+ if err := config.DB.Save(&company).Error; err != nil {
967
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan pengaturan"})
968
+ return
969
+ }
970
+
971
+ c.JSON(http.StatusOK, company)
972
+ }
controllers/order_controller.go CHANGED
@@ -2,6 +2,7 @@ package controllers
2
 
3
  import (
4
  "encoding/json"
 
5
  "fmt"
6
  "io"
7
  "log"
@@ -44,6 +45,9 @@ type OrderResponse struct {
44
  BranchID *uint `json:"branch_id"`
45
  BranchName string `json:"branch_name"`
46
  TotalAmount int `json:"total_amount"`
 
 
 
47
  PlatformFee int `json:"platform_fee"`
48
  PaidAmount int `json:"paid_amount"`
49
  ChangeAmount int `json:"change_amount"`
@@ -53,15 +57,21 @@ type OrderResponse struct {
53
  MidtransToken string `json:"midtrans_token"`
54
  Items []OrderItemResponse `json:"items"`
55
  CreatedAt time.Time `json:"created_at"`
 
 
 
 
 
56
  }
57
 
58
  type CashPaymentRequest struct {
59
  OrderID uint `json:"order_id" binding:"required"`
60
- PaidAmount int `json:"paid_amount" binding:"required,gt=0"`
61
  CustomerName string `json:"customer_name"`
62
  CustomerPhone string `json:"customer_phone"`
63
  ServedBy string `json:"served_by"`
64
  MemberID *uint `json:"member_id"`
 
65
  }
66
 
67
  type MidtransPaymentRequest struct {
@@ -71,6 +81,7 @@ type MidtransPaymentRequest struct {
71
  CustomerPhone string `json:"customer_phone"`
72
  ServedBy string `json:"served_by"`
73
  MemberID *uint `json:"member_id"`
 
74
  }
75
  type PaymentStatusResponse struct {
76
  OrderID uint `json:"order_id"`
@@ -114,12 +125,20 @@ func toOrderResponse(order *models.Order) OrderResponse {
114
  bName = order.Branch.Name
115
  }
116
 
 
 
 
 
 
117
  return OrderResponse{
118
  ID: order.ID,
119
  OrderNumber: order.OrderNumber,
120
  BranchID: order.BranchID,
121
  BranchName: bName,
122
  TotalAmount: order.TotalAmount,
 
 
 
123
  PlatformFee: order.PlatformFee,
124
  PaidAmount: order.PaidAmount,
125
  ChangeAmount: order.ChangeAmount,
@@ -129,6 +148,11 @@ func toOrderResponse(order *models.Order) OrderResponse {
129
  MidtransToken: order.MidtransToken,
130
  Items: itemsResp,
131
  CreatedAt: order.CreatedAt,
 
 
 
 
 
132
  }
133
  }
134
 
@@ -223,6 +247,7 @@ func CreateOrder(c *gin.Context) {
223
  }
224
 
225
  // Update total amount order
 
226
  order.TotalAmount = total
227
  if err := tx.Save(&order).Error; err != nil {
228
  tx.Rollback()
@@ -237,7 +262,7 @@ func CreateOrder(c *gin.Context) {
237
  }
238
 
239
  // Load relationships untuk response
240
- config.DB.Preload("Branch").Preload("Items.Product").First(&order, order.ID)
241
 
242
  c.JSON(http.StatusCreated, toOrderResponse(&order))
243
  }
@@ -255,10 +280,13 @@ func ListOrders(c *gin.Context) {
255
  skip, _ := strconv.Atoi(skipStr)
256
  limit, _ := strconv.Atoi(limitStr)
257
 
258
- query := config.DB.Select("id, company_id, branch_id, order_number, total_amount, platform_fee, paid_amount, change_amount, payment_method, payment_status, midtrans_order_id, midtrans_token, created_at").
259
  Preload("Branch", func(db *gorm.DB) *gorm.DB {
260
  return db.Select("id, name")
261
  }).
 
 
 
262
  Preload("Items", func(db *gorm.DB) *gorm.DB {
263
  return db.Select("id, order_id, product_id, quantity, unit_price, subtotal")
264
  }).
@@ -317,7 +345,7 @@ func GetOrder(c *gin.Context) {
317
  }
318
 
319
  var order models.Order
320
- if err := config.DB.Preload("Branch").Preload("Items.Product").Where("id = ? AND company_id = ?", orderID, *user.CompanyID).First(&order).Error; err != nil {
321
  c.JSON(http.StatusNotFound, gin.H{"detail": "Order tidak ditemukan"})
322
  return
323
  }
@@ -371,6 +399,12 @@ func PayCash(c *gin.Context) {
371
  return
372
  }
373
 
 
 
 
 
 
 
374
  if req.PaidAmount < order.TotalAmount {
375
  tx.Rollback()
376
  c.JSON(http.StatusBadRequest, gin.H{
@@ -394,16 +428,27 @@ func PayCash(c *gin.Context) {
394
  if req.MemberID != nil {
395
  order.MemberID = req.MemberID
396
  }
 
 
 
 
 
 
397
 
398
  // Hitung fee platform berdasarkan Company
399
  fee := 0
 
 
 
 
 
400
  if company.CashFeeRules != "" && company.CashFeeRules != "[]" {
401
  var rules []map[string]interface{}
402
  if err := json.Unmarshal([]byte(company.CashFeeRules), &rules); err == nil {
403
  for _, r := range rules {
404
  minAmt := int(r["min_amount"].(float64))
405
  maxAmt := int(r["max_amount"].(float64))
406
- if order.TotalAmount >= minAmt && order.TotalAmount <= maxAmt {
407
  fee = int(r["fee"].(float64))
408
  break
409
  }
@@ -412,7 +457,7 @@ func PayCash(c *gin.Context) {
412
  }
413
  // Fallback ke fee lama jika tidak ada rule yang cocok/diset
414
  if fee == 0 && company.FeePercentage > 0 {
415
- fee = int(float64(order.TotalAmount) * company.FeePercentage / 100)
416
  }
417
  order.PlatformFee = fee
418
 
@@ -470,10 +515,12 @@ func PayCash(c *gin.Context) {
470
  }
471
 
472
  var branchName string
 
473
  if order.BranchID != nil {
474
  var branch models.Branch
475
  if err := config.DB.First(&branch, *order.BranchID).Error; err == nil {
476
  branchName = branch.Name
 
477
  }
478
  }
479
 
@@ -487,6 +534,11 @@ func PayCash(c *gin.Context) {
487
  order.ChangeAmount,
488
  company.Name,
489
  branchName,
 
 
 
 
 
490
  )
491
  }()
492
  }
@@ -536,6 +588,11 @@ func PayTripay(c *gin.Context) {
536
  return
537
  }
538
 
 
 
 
 
 
539
  if order.TotalAmount < company.QrisMinTransaction && !company.AllowQrisBelowMin {
540
  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))})
541
  return
@@ -746,7 +803,12 @@ func TripayWebhook(c *gin.Context) {
746
  if order.PaymentStatus == "paid" && oldStatus != "paid" {
747
  var company models.Company
748
  if err := tx.First(&company, order.CompanyID).Error; err == nil {
749
- fee := int((float64(order.TotalAmount) * company.QrisFeePercentage / 100) + float64(company.QrisFeeFixed))
 
 
 
 
 
750
  if fee < company.QrisFeeMin {
751
  fee = company.QrisFeeMin
752
  }
@@ -805,10 +867,12 @@ func TripayWebhook(c *gin.Context) {
805
  config.DB.First(&company, order.CompanyID)
806
 
807
  var branchName string
 
808
  if order.BranchID != nil {
809
  var branch models.Branch
810
  if err := config.DB.First(&branch, *order.BranchID).Error; err == nil {
811
  branchName = branch.Name
 
812
  }
813
  }
814
 
@@ -823,6 +887,11 @@ func TripayWebhook(c *gin.Context) {
823
  0,
824
  company.Name,
825
  branchName,
 
 
 
 
 
826
  )
827
  }
828
  }()
@@ -954,7 +1023,8 @@ func restoreStock(tx *gorm.DB, order *models.Order) {
954
  }
955
 
956
  func awardPoints(tx *gorm.DB, order *models.Order, company *models.Company) {
957
- if order.MemberID == nil {
 
958
  return
959
  }
960
 
@@ -1020,3 +1090,66 @@ func awardPoints(tx *gorm.DB, order *models.Order, company *models.Company) {
1020
  }
1021
  }
1022
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import (
4
  "encoding/json"
5
+ "errors"
6
  "fmt"
7
  "io"
8
  "log"
 
45
  BranchID *uint `json:"branch_id"`
46
  BranchName string `json:"branch_name"`
47
  TotalAmount int `json:"total_amount"`
48
+ SubtotalAmount int `json:"subtotal_amount"`
49
+ DiscountAmount int `json:"discount_amount"`
50
+ PointsUsed int `json:"points_used"`
51
  PlatformFee int `json:"platform_fee"`
52
  PaidAmount int `json:"paid_amount"`
53
  ChangeAmount int `json:"change_amount"`
 
57
  MidtransToken string `json:"midtrans_token"`
58
  Items []OrderItemResponse `json:"items"`
59
  CreatedAt time.Time `json:"created_at"`
60
+ ServedBy string `json:"served_by"`
61
+ CustomerName string `json:"customer_name"`
62
+ MemberID *uint `json:"member_id"`
63
+ MemberName string `json:"member_name"`
64
+ VoucherID *uint `json:"voucher_id"`
65
  }
66
 
67
  type CashPaymentRequest struct {
68
  OrderID uint `json:"order_id" binding:"required"`
69
+ PaidAmount int `json:"paid_amount" binding:"gte=0"`
70
  CustomerName string `json:"customer_name"`
71
  CustomerPhone string `json:"customer_phone"`
72
  ServedBy string `json:"served_by"`
73
  MemberID *uint `json:"member_id"`
74
+ VoucherID *uint `json:"voucher_id"`
75
  }
76
 
77
  type MidtransPaymentRequest struct {
 
81
  CustomerPhone string `json:"customer_phone"`
82
  ServedBy string `json:"served_by"`
83
  MemberID *uint `json:"member_id"`
84
+ VoucherID *uint `json:"voucher_id"`
85
  }
86
  type PaymentStatusResponse struct {
87
  OrderID uint `json:"order_id"`
 
125
  bName = order.Branch.Name
126
  }
127
 
128
+ mName := ""
129
+ if order.Member != nil {
130
+ mName = order.Member.Name
131
+ }
132
+
133
  return OrderResponse{
134
  ID: order.ID,
135
  OrderNumber: order.OrderNumber,
136
  BranchID: order.BranchID,
137
  BranchName: bName,
138
  TotalAmount: order.TotalAmount,
139
+ SubtotalAmount: order.SubtotalAmount,
140
+ DiscountAmount: order.DiscountAmount,
141
+ PointsUsed: order.PointsUsed,
142
  PlatformFee: order.PlatformFee,
143
  PaidAmount: order.PaidAmount,
144
  ChangeAmount: order.ChangeAmount,
 
148
  MidtransToken: order.MidtransToken,
149
  Items: itemsResp,
150
  CreatedAt: order.CreatedAt,
151
+ ServedBy: order.ServedBy,
152
+ CustomerName: order.CustomerName,
153
+ MemberID: order.MemberID,
154
+ MemberName: mName,
155
+ VoucherID: order.VoucherID,
156
  }
157
  }
158
 
 
247
  }
248
 
249
  // Update total amount order
250
+ order.SubtotalAmount = total
251
  order.TotalAmount = total
252
  if err := tx.Save(&order).Error; err != nil {
253
  tx.Rollback()
 
262
  }
263
 
264
  // Load relationships untuk response
265
+ config.DB.Preload("Branch").Preload("Member").Preload("Items.Product").First(&order, order.ID)
266
 
267
  c.JSON(http.StatusCreated, toOrderResponse(&order))
268
  }
 
280
  skip, _ := strconv.Atoi(skipStr)
281
  limit, _ := strconv.Atoi(limitStr)
282
 
283
+ query := config.DB.Select("id, company_id, branch_id, order_number, total_amount, platform_fee, paid_amount, change_amount, payment_method, payment_status, midtrans_order_id, midtrans_token, created_at, served_by, customer_name, member_id, voucher_id, discount_amount, points_used").
284
  Preload("Branch", func(db *gorm.DB) *gorm.DB {
285
  return db.Select("id, name")
286
  }).
287
+ Preload("Member", func(db *gorm.DB) *gorm.DB {
288
+ return db.Select("id, name")
289
+ }).
290
  Preload("Items", func(db *gorm.DB) *gorm.DB {
291
  return db.Select("id, order_id, product_id, quantity, unit_price, subtotal")
292
  }).
 
345
  }
346
 
347
  var order models.Order
348
+ if err := config.DB.Preload("Branch").Preload("Member").Preload("Items.Product").Where("id = ? AND company_id = ?", orderID, *user.CompanyID).First(&order).Error; err != nil {
349
  c.JSON(http.StatusNotFound, gin.H{"detail": "Order tidak ditemukan"})
350
  return
351
  }
 
399
  return
400
  }
401
 
402
+ if err := applyVoucher(tx, &order, req.VoucherID, req.MemberID, company.ID); err != nil {
403
+ tx.Rollback()
404
+ c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
405
+ return
406
+ }
407
+
408
  if req.PaidAmount < order.TotalAmount {
409
  tx.Rollback()
410
  c.JSON(http.StatusBadRequest, gin.H{
 
428
  if req.MemberID != nil {
429
  order.MemberID = req.MemberID
430
  }
431
+ if req.CustomerName != "" {
432
+ order.CustomerName = req.CustomerName
433
+ }
434
+ if req.CustomerPhone != "" {
435
+ order.CustomerPhone = req.CustomerPhone
436
+ }
437
 
438
  // Hitung fee platform berdasarkan Company
439
  fee := 0
440
+ baseAmountForFee := order.TotalAmount
441
+ if company.ChargeFeeBeforeDiscount {
442
+ baseAmountForFee = order.SubtotalAmount
443
+ }
444
+
445
  if company.CashFeeRules != "" && company.CashFeeRules != "[]" {
446
  var rules []map[string]interface{}
447
  if err := json.Unmarshal([]byte(company.CashFeeRules), &rules); err == nil {
448
  for _, r := range rules {
449
  minAmt := int(r["min_amount"].(float64))
450
  maxAmt := int(r["max_amount"].(float64))
451
+ if baseAmountForFee >= minAmt && baseAmountForFee <= maxAmt {
452
  fee = int(r["fee"].(float64))
453
  break
454
  }
 
457
  }
458
  // Fallback ke fee lama jika tidak ada rule yang cocok/diset
459
  if fee == 0 && company.FeePercentage > 0 {
460
+ fee = int(float64(baseAmountForFee) * company.FeePercentage / 100)
461
  }
462
  order.PlatformFee = fee
463
 
 
515
  }
516
 
517
  var branchName string
518
+ var branchAddress string
519
  if order.BranchID != nil {
520
  var branch models.Branch
521
  if err := config.DB.First(&branch, *order.BranchID).Error; err == nil {
522
  branchName = branch.Name
523
+ branchAddress = branch.Address
524
  }
525
  }
526
 
 
534
  order.ChangeAmount,
535
  company.Name,
536
  branchName,
537
+ branchAddress,
538
+ order.SubtotalAmount,
539
+ order.DiscountAmount,
540
+ req.ServedBy,
541
+ order.PointsEarned,
542
  )
543
  }()
544
  }
 
588
  return
589
  }
590
 
591
+ if err := applyVoucher(config.DB, &order, req.VoucherID, req.MemberID, company.ID); err != nil {
592
+ c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
593
+ return
594
+ }
595
+
596
  if order.TotalAmount < company.QrisMinTransaction && !company.AllowQrisBelowMin {
597
  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))})
598
  return
 
803
  if order.PaymentStatus == "paid" && oldStatus != "paid" {
804
  var company models.Company
805
  if err := tx.First(&company, order.CompanyID).Error; err == nil {
806
+ baseAmountForFee := order.TotalAmount
807
+ if company.ChargeFeeBeforeDiscount {
808
+ baseAmountForFee = order.SubtotalAmount
809
+ }
810
+
811
+ fee := int((float64(baseAmountForFee) * company.QrisFeePercentage / 100) + float64(company.QrisFeeFixed))
812
  if fee < company.QrisFeeMin {
813
  fee = company.QrisFeeMin
814
  }
 
867
  config.DB.First(&company, order.CompanyID)
868
 
869
  var branchName string
870
+ var branchAddress string
871
  if order.BranchID != nil {
872
  var branch models.Branch
873
  if err := config.DB.First(&branch, *order.BranchID).Error; err == nil {
874
  branchName = branch.Name
875
+ branchAddress = branch.Address
876
  }
877
  }
878
 
 
887
  0,
888
  company.Name,
889
  branchName,
890
+ branchAddress,
891
+ order.SubtotalAmount,
892
+ order.DiscountAmount,
893
+ order.ServedBy,
894
+ order.PointsEarned,
895
  )
896
  }
897
  }()
 
1023
  }
1024
 
1025
  func awardPoints(tx *gorm.DB, order *models.Order, company *models.Company) {
1026
+ // Skip earning points if voucher is used
1027
+ if order.MemberID == nil || order.VoucherID != nil {
1028
  return
1029
  }
1030
 
 
1090
  }
1091
  }
1092
  }
1093
+
1094
+ func applyVoucher(tx *gorm.DB, order *models.Order, voucherID *uint, memberID *uint, companyID uint) error {
1095
+ if voucherID == nil {
1096
+ return nil
1097
+ }
1098
+ if memberID == nil {
1099
+ return errors.New("Voucher hanya dapat digunakan oleh Member")
1100
+ }
1101
+
1102
+ var voucher models.Voucher
1103
+ if err := tx.Where("id = ? AND company_id = ? AND is_active = true", *voucherID, companyID).First(&voucher).Error; err != nil {
1104
+ return errors.New("Voucher tidak ditemukan atau tidak aktif")
1105
+ }
1106
+
1107
+ if voucher.BranchID != nil && order.BranchID != nil && *voucher.BranchID != *order.BranchID {
1108
+ return errors.New("Voucher ini tidak berlaku untuk cabang ini")
1109
+ }
1110
+
1111
+ if order.TotalAmount < voucher.MinPurchase {
1112
+ return errors.New("Total belanja belum memenuhi syarat minimal voucher")
1113
+ }
1114
+
1115
+ var member models.Member
1116
+ if err := tx.Where("id = ? AND company_id = ?", *memberID, companyID).First(&member).Error; err != nil {
1117
+ return errors.New("Member tidak ditemukan")
1118
+ }
1119
+
1120
+ if member.Points < voucher.PointCost {
1121
+ return errors.New("Poin member tidak mencukupi untuk menggunakan voucher ini")
1122
+ }
1123
+
1124
+ // Kurangi poin member
1125
+ if voucher.PointCost > 0 {
1126
+ member.Points -= voucher.PointCost
1127
+ if err := tx.Save(&member).Error; err != nil {
1128
+ return errors.New("Gagal memotong poin member")
1129
+ }
1130
+
1131
+ // Catat history
1132
+ history := models.PointHistory{
1133
+ CompanyID: companyID,
1134
+ MemberID: member.ID,
1135
+ PointsDelta: -voucher.PointCost,
1136
+ Reason: "Tukar poin untuk voucher " + voucher.Name,
1137
+ }
1138
+ if err := tx.Create(&history).Error; err != nil {
1139
+ return errors.New("Gagal mencatat histori poin")
1140
+ }
1141
+ }
1142
+
1143
+ // Terapkan diskon
1144
+ discount := voucher.DiscountValue
1145
+ if discount > order.TotalAmount {
1146
+ discount = order.TotalAmount
1147
+ }
1148
+
1149
+ order.TotalAmount -= discount
1150
+ order.DiscountAmount = discount
1151
+ order.VoucherID = voucherID
1152
+ order.PointsUsed = voucher.PointCost
1153
+
1154
+ return nil
1155
+ }
controllers/report_controller.go CHANGED
@@ -26,11 +26,12 @@ type SummaryTopProduct struct {
26
  }
27
 
28
  type ReportOrderRow struct {
29
- OrderNumber string `json:"order_number"`
30
- TotalAmount int `json:"total_amount"`
31
- PaymentMethod string `json:"payment_method"`
32
- CreatedAt time.Time `json:"created_at"`
33
- Items string `json:"items"`
 
34
  }
35
 
36
  func getDayRange(targetDate time.Time) (time.Time, time.Time) {
@@ -125,11 +126,12 @@ func DailyReport(c *gin.Context) {
125
  itemsStr := strings.Join(itemNames, ", ")
126
 
127
  orderRows = append(orderRows, ReportOrderRow{
128
- OrderNumber: o.OrderNumber,
129
- TotalAmount: o.TotalAmount,
130
- PaymentMethod: o.PaymentMethod,
131
- CreatedAt: o.CreatedAt,
132
- Items: itemsStr,
 
133
  })
134
  }
135
 
 
26
  }
27
 
28
  type ReportOrderRow struct {
29
+ OrderNumber string `json:"order_number"`
30
+ TotalAmount int `json:"total_amount"`
31
+ SubtotalAmount int `json:"subtotal_amount"`
32
+ PaymentMethod string `json:"payment_method"`
33
+ CreatedAt time.Time `json:"created_at"`
34
+ Items string `json:"items"`
35
  }
36
 
37
  func getDayRange(targetDate time.Time) (time.Time, time.Time) {
 
126
  itemsStr := strings.Join(itemNames, ", ")
127
 
128
  orderRows = append(orderRows, ReportOrderRow{
129
+ OrderNumber: o.OrderNumber,
130
+ TotalAmount: o.TotalAmount,
131
+ SubtotalAmount: o.SubtotalAmount,
132
+ PaymentMethod: o.PaymentMethod,
133
+ CreatedAt: o.CreatedAt,
134
+ Items: itemsStr,
135
  })
136
  }
137
 
controllers/voucher_controller.go ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package controllers
2
+
3
+ import (
4
+ "net/http"
5
+ "service-warungpos-go/config"
6
+ "service-warungpos-go/models"
7
+ "strconv"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ // List Vouchers
13
+ func GetVouchers(c *gin.Context) {
14
+ userVal, _ := c.Get("user")
15
+ user := userVal.(*models.User)
16
+
17
+ var vouchers []models.Voucher
18
+ query := config.DB.Where("company_id = ?", *user.CompanyID)
19
+
20
+ // Filter by branch_id if provided
21
+ branchIDStr := c.Query("branch_id")
22
+ if branchIDStr != "" {
23
+ if bID, err := strconv.Atoi(branchIDStr); err == nil {
24
+ // Jika voucher memiliki branch_id spesifik, atau branch_id null (berlaku semua cabang)
25
+ query = query.Where("branch_id = ? OR branch_id IS NULL", bID)
26
+ }
27
+ }
28
+
29
+ if err := query.Order("created_at desc").Find(&vouchers).Error; err != nil {
30
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data voucher"})
31
+ return
32
+ }
33
+
34
+ c.JSON(http.StatusOK, vouchers)
35
+ }
36
+
37
+ // Create Voucher
38
+ type CreateVoucherRequest struct {
39
+ Name string `json:"name" binding:"required"`
40
+ DiscountValue int `json:"discount_value" binding:"required,gt=0"`
41
+ PointCost int `json:"point_cost" binding:"required,gt=0"`
42
+ MinPurchase int `json:"min_purchase"`
43
+ BranchID *uint `json:"branch_id"`
44
+ IsActive *bool `json:"is_active"`
45
+ }
46
+
47
+ func CreateVoucher(c *gin.Context) {
48
+ userVal, _ := c.Get("user")
49
+ user := userVal.(*models.User)
50
+
51
+ if user.Role != "owner" {
52
+ c.JSON(http.StatusForbidden, gin.H{"detail": "Akses ditolak"})
53
+ return
54
+ }
55
+
56
+ var req CreateVoucherRequest
57
+ if err := c.ShouldBindJSON(&req); err != nil {
58
+ c.JSON(http.StatusBadRequest, gin.H{"detail": "Data tidak valid"})
59
+ return
60
+ }
61
+
62
+ isActive := true
63
+ if req.IsActive != nil {
64
+ isActive = *req.IsActive
65
+ }
66
+
67
+ voucher := models.Voucher{
68
+ CompanyID: *user.CompanyID,
69
+ Name: req.Name,
70
+ DiscountValue: req.DiscountValue,
71
+ PointCost: req.PointCost,
72
+ MinPurchase: req.MinPurchase,
73
+ BranchID: req.BranchID,
74
+ IsActive: isActive,
75
+ }
76
+
77
+ if err := config.DB.Create(&voucher).Error; err != nil {
78
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan voucher"})
79
+ return
80
+ }
81
+
82
+ c.JSON(http.StatusCreated, voucher)
83
+ }
84
+
85
+ // Update Voucher
86
+ func UpdateVoucher(c *gin.Context) {
87
+ userVal, _ := c.Get("user")
88
+ user := userVal.(*models.User)
89
+
90
+ if user.Role != "owner" {
91
+ c.JSON(http.StatusForbidden, gin.H{"detail": "Akses ditolak"})
92
+ return
93
+ }
94
+
95
+ id := c.Param("id")
96
+ var voucher models.Voucher
97
+ if err := config.DB.Where("id = ? AND company_id = ?", id, *user.CompanyID).First(&voucher).Error; err != nil {
98
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Voucher tidak ditemukan"})
99
+ return
100
+ }
101
+
102
+ var req CreateVoucherRequest
103
+ if err := c.ShouldBindJSON(&req); err != nil {
104
+ c.JSON(http.StatusBadRequest, gin.H{"detail": "Data tidak valid"})
105
+ return
106
+ }
107
+
108
+ voucher.Name = req.Name
109
+ voucher.DiscountValue = req.DiscountValue
110
+ voucher.PointCost = req.PointCost
111
+ voucher.MinPurchase = req.MinPurchase
112
+ voucher.BranchID = req.BranchID
113
+ if req.IsActive != nil {
114
+ voucher.IsActive = *req.IsActive
115
+ }
116
+
117
+ if err := config.DB.Save(&voucher).Error; err != nil {
118
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengupdate voucher"})
119
+ return
120
+ }
121
+
122
+ c.JSON(http.StatusOK, voucher)
123
+ }
124
+
125
+ // Delete Voucher
126
+ func DeleteVoucher(c *gin.Context) {
127
+ userVal, _ := c.Get("user")
128
+ user := userVal.(*models.User)
129
+
130
+ if user.Role != "owner" {
131
+ c.JSON(http.StatusForbidden, gin.H{"detail": "Akses ditolak"})
132
+ return
133
+ }
134
+
135
+ id := c.Param("id")
136
+ if err := config.DB.Where("id = ? AND company_id = ?", id, *user.CompanyID).Delete(&models.Voucher{}).Error; err != nil {
137
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus voucher"})
138
+ return
139
+ }
140
+
141
+ c.JSON(http.StatusOK, gin.H{"detail": "Voucher berhasil dihapus"})
142
+ }
main.go CHANGED
@@ -39,6 +39,7 @@ func main() {
39
  &models.PointRule{},
40
  &models.WalletTransaction{},
41
  &models.TopupRequest{},
 
42
  &models.PointHistory{},
43
  )
44
  if err != nil {
@@ -113,6 +114,8 @@ func main() {
113
  auth.GET("/users", middleware.RequireSuperAdmin(), controllers.ListUsers)
114
  auth.PUT("/users/:user_id", middleware.RequireSuperAdmin(), controllers.UpdateUser)
115
 
 
 
116
  // Owner routes for kasir
117
  auth.GET("/kasir", middleware.RequireOwner(), controllers.ListKasir)
118
  auth.POST("/kasir", middleware.RequireOwner(), controllers.CreateKasir)
@@ -198,9 +201,22 @@ func main() {
198
  {
199
  orders.Use(middleware.GetCurrentUser(), middleware.RequireTenantContext())
200
  {
201
- orders.POST("/", controllers.CreateOrder)
202
  orders.GET("/", controllers.ListOrders)
203
  orders.GET("/:order_id", controllers.GetOrder)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  }
205
  }
206
 
@@ -267,6 +283,7 @@ func main() {
267
  adminCompanies.Use(middleware.GetCurrentUser(), middleware.RequireSuperAdmin())
268
  {
269
  adminCompanies.POST("/:id/adjust-wallet", controllers.AdminAdjustWallet)
 
270
  }
271
  }
272
  }
 
39
  &models.PointRule{},
40
  &models.WalletTransaction{},
41
  &models.TopupRequest{},
42
+ &models.Voucher{},
43
  &models.PointHistory{},
44
  )
45
  if err != nil {
 
114
  auth.GET("/users", middleware.RequireSuperAdmin(), controllers.ListUsers)
115
  auth.PUT("/users/:user_id", middleware.RequireSuperAdmin(), controllers.UpdateUser)
116
 
117
+
118
+
119
  // Owner routes for kasir
120
  auth.GET("/kasir", middleware.RequireOwner(), controllers.ListKasir)
121
  auth.POST("/kasir", middleware.RequireOwner(), controllers.CreateKasir)
 
201
  {
202
  orders.Use(middleware.GetCurrentUser(), middleware.RequireTenantContext())
203
  {
 
204
  orders.GET("/", controllers.ListOrders)
205
  orders.GET("/:order_id", controllers.GetOrder)
206
+ orders.POST("/", controllers.CreateOrder)
207
+ orders.POST("/pay/cash", controllers.PayCash)
208
+ orders.POST("/pay/midtrans", controllers.PayTripay)
209
+ }
210
+ }
211
+
212
+ vouchers := api.Group("/vouchers")
213
+ {
214
+ vouchers.Use(middleware.GetCurrentUser())
215
+ {
216
+ vouchers.GET("/", controllers.GetVouchers)
217
+ vouchers.POST("/", controllers.CreateVoucher)
218
+ vouchers.PUT("/:id", controllers.UpdateVoucher)
219
+ vouchers.DELETE("/:id", controllers.DeleteVoucher)
220
  }
221
  }
222
 
 
283
  adminCompanies.Use(middleware.GetCurrentUser(), middleware.RequireSuperAdmin())
284
  {
285
  adminCompanies.POST("/:id/adjust-wallet", controllers.AdminAdjustWallet)
286
+ adminCompanies.PUT("/:id/settings", controllers.AdminUpdateCompanySettings)
287
  }
288
  }
289
  }
models/models.go CHANGED
@@ -27,6 +27,7 @@ type Company struct {
27
  QrisFeeMin int `gorm:"default:850" json:"qris_fee_min"`
28
  QrisMinTransaction int `gorm:"default:10000" json:"qris_min_transaction"`
29
  AllowQrisBelowMin bool `gorm:"default:false" json:"allow_qris_below_min"`
 
30
  WalletBalance float64 `gorm:"type:float;default:0.0" json:"wallet_balance"`
31
  WalletLimit float64 `gorm:"type:float;default:-50000.0" json:"wallet_limit"`
32
  PointRules []PointRule `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"point_rules"`
@@ -151,6 +152,7 @@ type Order struct {
151
  CompanyID uint `gorm:"not null;index" json:"company_id"`
152
  BranchID *uint `gorm:"index" json:"branch_id"`
153
  OrderNumber string `gorm:"type:varchar(50);not null;index" json:"order_number"`
 
154
  TotalAmount int `gorm:"not null;default:0" json:"total_amount"`
155
  PaidAmount int `gorm:"not null;default:0" json:"paid_amount"`
156
  ChangeAmount int `gorm:"not null;default:0" json:"change_amount"`
@@ -163,12 +165,29 @@ type Order struct {
163
  Company *Company `gorm:"foreignKey:CompanyID" json:"company,omitempty"`
164
  Branch *Branch `gorm:"foreignKey:BranchID;constraint:OnDelete:SET NULL;" json:"branch,omitempty"`
165
  MemberID *uint `gorm:"index" json:"member_id"`
166
- Member *Member `gorm:"foreignKey:MemberID;references:ID;constraint:-" json:"member,omitempty"`
167
  ServedBy string `gorm:"type:varchar(100)" json:"served_by"`
168
  CustomerName string `gorm:"type:varchar(100)" json:"customer_name"`
169
  CustomerPhone string `gorm:"type:varchar(50)" json:"customer_phone"`
170
  PointsEarned int `gorm:"default:0" json:"points_earned"`
171
  Items []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE" json:"items"`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
 
174
  type OrderItem struct {
 
27
  QrisFeeMin int `gorm:"default:850" json:"qris_fee_min"`
28
  QrisMinTransaction int `gorm:"default:10000" json:"qris_min_transaction"`
29
  AllowQrisBelowMin bool `gorm:"default:false" json:"allow_qris_below_min"`
30
+ ChargeFeeBeforeDiscount bool `gorm:"default:false" json:"charge_fee_before_discount"`
31
  WalletBalance float64 `gorm:"type:float;default:0.0" json:"wallet_balance"`
32
  WalletLimit float64 `gorm:"type:float;default:-50000.0" json:"wallet_limit"`
33
  PointRules []PointRule `gorm:"foreignKey:CompanyID;constraint:OnDelete:CASCADE" json:"point_rules"`
 
152
  CompanyID uint `gorm:"not null;index" json:"company_id"`
153
  BranchID *uint `gorm:"index" json:"branch_id"`
154
  OrderNumber string `gorm:"type:varchar(50);not null;index" json:"order_number"`
155
+ SubtotalAmount int `gorm:"not null;default:0" json:"subtotal_amount"`
156
  TotalAmount int `gorm:"not null;default:0" json:"total_amount"`
157
  PaidAmount int `gorm:"not null;default:0" json:"paid_amount"`
158
  ChangeAmount int `gorm:"not null;default:0" json:"change_amount"`
 
165
  Company *Company `gorm:"foreignKey:CompanyID" json:"company,omitempty"`
166
  Branch *Branch `gorm:"foreignKey:BranchID;constraint:OnDelete:SET NULL;" json:"branch,omitempty"`
167
  MemberID *uint `gorm:"index" json:"member_id"`
168
+ Member *Member `gorm:"constraint:-" json:"member,omitempty"`
169
  ServedBy string `gorm:"type:varchar(100)" json:"served_by"`
170
  CustomerName string `gorm:"type:varchar(100)" json:"customer_name"`
171
  CustomerPhone string `gorm:"type:varchar(50)" json:"customer_phone"`
172
  PointsEarned int `gorm:"default:0" json:"points_earned"`
173
  Items []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE" json:"items"`
174
+ VoucherID *uint `gorm:"index" json:"voucher_id"`
175
+ Voucher *Voucher `gorm:"constraint:-" json:"voucher,omitempty"`
176
+ DiscountAmount int `gorm:"default:0" json:"discount_amount"`
177
+ PointsUsed int `gorm:"default:0" json:"points_used"`
178
+ }
179
+
180
+ type Voucher struct {
181
+ ID uint `gorm:"primaryKey" json:"id"`
182
+ CompanyID uint `gorm:"not null;index" json:"company_id"`
183
+ BranchID *uint `gorm:"index" json:"branch_id"`
184
+ Name string `gorm:"type:varchar(100);not null" json:"name"`
185
+ DiscountValue int `gorm:"not null;default:0" json:"discount_value"`
186
+ PointCost int `gorm:"not null;default:0" json:"point_cost"`
187
+ MinPurchase int `gorm:"not null;default:0" json:"min_purchase"`
188
+ IsActive bool `gorm:"default:true" json:"is_active"`
189
+ CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
190
+ UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
191
  }
192
 
193
  type OrderItem struct {
services/whatsapp.go CHANGED
@@ -64,6 +64,11 @@ func SendPaymentNotification(
64
  changeAmount int,
65
  storeName string,
66
  branchName string,
 
 
 
 
 
67
  ) bool {
68
  if config.GlobalConfig.FonnteToken == "" {
69
  log.Println("[WHATSAPP] Token Fonnte tidak dikonfigurasi, lewati kirim WhatsApp")
@@ -96,34 +101,79 @@ func SendPaymentNotification(
96
 
97
  // Buat isi pesan
98
  var lines []string
99
- lines = append(lines, "*"+displayName+"*")
100
- lines = append(lines, "_Struk Pembayaran_")
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  lines = append(lines, divider("-", 30))
102
 
103
- lines = append(lines, fmt.Sprintf("No. Order : ```%s```", orderNumber))
104
- lines = append(lines, fmt.Sprintf("Customer : %s", customerName))
105
- lines = append(lines, fmt.Sprintf("Metode : %s", methodLabel))
 
 
 
 
 
 
 
 
 
 
106
  lines = append(lines, divider("-", 30))
107
 
108
  if len(items) > 0 {
109
- lines = append(lines, "*Daftar Pesanan*")
110
  for _, item := range items {
111
  subtotal := item.Price * item.Quantity
112
- lines = append(lines, fmt.Sprintf(" %s x%d", item.Name, item.Quantity))
113
- lines = append(lines, fmt.Sprintf(" %20s", fmtRupiah(subtotal)))
114
  }
115
- lines = append(lines, divider("-", 30))
116
  }
117
 
118
- lines = append(lines, fmt.Sprintf("*Total : %s*", fmtRupiah(totalAmount)))
 
 
 
 
 
 
 
 
 
 
 
 
119
 
 
 
 
120
  if paymentMethod == "cash" {
121
- lines = append(lines, fmt.Sprintf("Dibayar : %s", fmtRupiah(totalAmount+changeAmount)))
122
- lines = append(lines, fmt.Sprintf("*Kembalian : %s*", fmtRupiah(changeAmount)))
 
 
 
123
  }
124
 
125
  lines = append(lines, divider("-", 30))
126
- lines = append(lines, fmt.Sprintf("_Terima kasih telah berbelanja di %s._", displayName))
 
 
 
 
 
127
 
128
  message := strings.Join(lines, "\n")
129
 
 
64
  changeAmount int,
65
  storeName string,
66
  branchName string,
67
+ branchAddress string,
68
+ subTotal int,
69
+ discountAmount int,
70
+ servedBy string,
71
+ pointsEarned int,
72
  ) bool {
73
  if config.GlobalConfig.FonnteToken == "" {
74
  log.Println("[WHATSAPP] Token Fonnte tidak dikonfigurasi, lewati kirim WhatsApp")
 
101
 
102
  // Buat isi pesan
103
  var lines []string
104
+ lines = append(lines, "Berikut adalah e-receipt untuk transaksi Anda")
105
+ lines = append(lines, orderNumber)
106
+ lines = append(lines, "")
107
+
108
+ now := time.Now()
109
+ // Gunakan WIB atau UTC
110
+ lines = append(lines, fmt.Sprintf("Pembayaran anda telah kami terima pada %s", now.Format("2006-01-02 15:04:05")))
111
+ lines = append(lines, "")
112
+ lines = append(lines, fmt.Sprintf("KWITANSI %s", strings.ToUpper(baseStore)))
113
+ lines = append(lines, "")
114
+ lines = append(lines, fmt.Sprintf("Outlet : %s", displayName))
115
+
116
+ if branchAddress != "" {
117
+ lines = append(lines, fmt.Sprintf("Alamat : %s", branchAddress))
118
+ }
119
  lines = append(lines, divider("-", 30))
120
 
121
+ lines = append(lines, fmt.Sprintf("No. Kwitansi : %s", orderNumber))
122
+ if customerName != "" {
123
+ lines = append(lines, fmt.Sprintf("Pelanggan : %s", customerName))
124
+ } else {
125
+ lines = append(lines, "Pelanggan : Umum")
126
+ }
127
+
128
+ if servedBy != "" {
129
+ lines = append(lines, fmt.Sprintf("Kasir : %s", servedBy))
130
+ }
131
+
132
+ lines = append(lines, fmt.Sprintf("Tanggal : %s", now.Format("2006-01-02 15:04:05")))
133
+ lines = append(lines, fmt.Sprintf("Pembayaran : %s", methodLabel))
134
  lines = append(lines, divider("-", 30))
135
 
136
  if len(items) > 0 {
137
+ lines = append(lines, "Layanan / Pesanan")
138
  for _, item := range items {
139
  subtotal := item.Price * item.Quantity
140
+ lines = append(lines, fmt.Sprintf("- %s x%d — %s", item.Name, item.Quantity, fmtRupiah(subtotal)))
 
141
  }
142
+ lines = append(lines, "")
143
  }
144
 
145
+ lines = append(lines, "Ringkasan Pembayaran")
146
+ if discountAmount > 0 {
147
+ lines = append(lines, "- Voucher : Ya")
148
+ } else {
149
+ lines = append(lines, "- Voucher : Tidak")
150
+ }
151
+
152
+ if subTotal > 0 {
153
+ lines = append(lines, fmt.Sprintf("- Sub Total : %s", fmtRupiah(subTotal)))
154
+ } else {
155
+ // Fallback
156
+ lines = append(lines, fmt.Sprintf("- Sub Total : %s", fmtRupiah(totalAmount+discountAmount)))
157
+ }
158
 
159
+ lines = append(lines, fmt.Sprintf("- Potongan Voucher : %s", fmtRupiah(discountAmount)))
160
+ lines = append(lines, fmt.Sprintf("- Total Bayar : %s", fmtRupiah(totalAmount)))
161
+
162
  if paymentMethod == "cash" {
163
+ lines = append(lines, fmt.Sprintf("- Kembalian : %s", fmtRupiah(changeAmount)))
164
+ }
165
+
166
+ if pointsEarned > 0 {
167
+ lines = append(lines, fmt.Sprintf("- Poin Didapat : %d Poin", pointsEarned))
168
  }
169
 
170
  lines = append(lines, divider("-", 30))
171
+ lines = append(lines, "Terima kasih telah menggunakan jasa kami.")
172
+ lines = append(lines, "")
173
+ lines = append(lines, "Hormat kami,")
174
+ lines = append(lines, "")
175
+ lines = append(lines, displayName)
176
+ lines = append(lines, "Jika Anda tidak merasa melakukan transaksi ini, segera hubungi kami.")
177
 
178
  message := strings.Join(lines, "\n")
179