package services import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "strings" "time" "service-warungpos-go/config" ) const FonnteAPIURL = "https://api.fonnte.com/send" func formatPhone(phone string) string { phone = strings.TrimSpace(phone) phone = strings.ReplaceAll(phone, " ", "") phone = strings.ReplaceAll(phone, "-", "") if strings.HasPrefix(phone, "0") { phone = "62" + phone[1:] } else if strings.HasPrefix(phone, "+") { phone = phone[1:] } return phone } func fmtRupiah(amount int) string { // Format ribuan dengan titik (contoh: 15000 menjadi Rp 15.000) str := fmt.Sprintf("%d", amount) var result []string length := len(str) for i := length; i > 0; i -= 3 { start := i - 3 if start < 0 { start = 0 } result = append([]string{str[start:i]}, result...) } return "Rp " + strings.Join(result, ".") } func divider(char string, length int) string { return strings.Repeat(char, length) } type WAItem struct { Name string `json:"name"` Quantity int `json:"quantity"` Price int `json:"price"` } func SendPaymentNotification( phone string, orderNumber string, totalAmount int, paymentMethod string, customerName string, items []WAItem, changeAmount int, storeName string, branchName string, branchAddress string, subTotal int, discountAmount int, servedBy string, pointsEarned int, ) bool { if config.GlobalConfig.FonnteToken == "" { log.Println("[WHATSAPP] Token Fonnte tidak dikonfigurasi, lewati kirim WhatsApp") return false } if phone == "" { log.Println("[WHATSAPP] Nomor telepon customer kosong, lewati kirim WhatsApp") return false } formattedPhone := formatPhone(phone) baseStore := storeName if baseStore == "" { baseStore = config.GlobalConfig.StoreName } if baseStore == "" { baseStore = "XOGM Go" } displayName := baseStore if branchName != "" { displayName = baseStore + " - " + branchName } methodLabel := "Nontunai" if paymentMethod == "cash" { methodLabel = "Tunai (Cash)" } // Buat isi pesan var lines []string lines = append(lines, "Berikut adalah e-receipt untuk transaksi Anda") lines = append(lines, orderNumber) lines = append(lines, "") now := time.Now() // Gunakan WIB atau UTC lines = append(lines, fmt.Sprintf("Pembayaran anda telah kami terima pada %s", now.Format("2006-01-02 15:04:05"))) lines = append(lines, "") lines = append(lines, fmt.Sprintf("KWITANSI %s", strings.ToUpper(baseStore))) lines = append(lines, "") lines = append(lines, fmt.Sprintf("Outlet : %s", displayName)) if branchAddress != "" { lines = append(lines, fmt.Sprintf("Alamat : %s", branchAddress)) } lines = append(lines, divider("-", 30)) lines = append(lines, fmt.Sprintf("No. Kwitansi : %s", orderNumber)) if customerName != "" { lines = append(lines, fmt.Sprintf("Pelanggan : %s", customerName)) } else { lines = append(lines, "Pelanggan : Umum") } if servedBy != "" { lines = append(lines, fmt.Sprintf("Kasir : %s", servedBy)) } lines = append(lines, fmt.Sprintf("Tanggal : %s", now.Format("2006-01-02 15:04:05"))) lines = append(lines, fmt.Sprintf("Pembayaran : %s", methodLabel)) lines = append(lines, divider("-", 30)) if len(items) > 0 { lines = append(lines, "Layanan / Pesanan") for _, item := range items { subtotal := item.Price * item.Quantity lines = append(lines, fmt.Sprintf("- %s x%d — %s", item.Name, item.Quantity, fmtRupiah(subtotal))) } lines = append(lines, "") } lines = append(lines, "Ringkasan Pembayaran") if discountAmount > 0 { lines = append(lines, "- Voucher : Ya") } else { lines = append(lines, "- Voucher : Tidak") } if subTotal > 0 { lines = append(lines, fmt.Sprintf("- Sub Total : %s", fmtRupiah(subTotal))) } else { // Fallback lines = append(lines, fmt.Sprintf("- Sub Total : %s", fmtRupiah(totalAmount+discountAmount))) } lines = append(lines, fmt.Sprintf("- Potongan Voucher : %s", fmtRupiah(discountAmount))) lines = append(lines, fmt.Sprintf("- Total Bayar : %s", fmtRupiah(totalAmount))) if paymentMethod == "cash" { lines = append(lines, fmt.Sprintf("- Kembalian : %s", fmtRupiah(changeAmount))) } if pointsEarned > 0 { lines = append(lines, fmt.Sprintf("- Poin Didapat : %d Poin", pointsEarned)) } lines = append(lines, divider("-", 30)) lines = append(lines, "Terima kasih telah menggunakan jasa kami.") lines = append(lines, "") lines = append(lines, "Hormat kami,") lines = append(lines, "") lines = append(lines, displayName) lines = append(lines, "Jika Anda tidak merasa melakukan transaksi ini, segera hubungi kami.") message := strings.Join(lines, "\n") return postFonnte(formattedPhone, message) } func SendOtpWa(phone string, otpCode string) bool { if config.GlobalConfig.FonnteToken == "" { return false } formattedPhone := formatPhone(phone) storeName := config.GlobalConfig.StoreName if storeName == "" { storeName = "XOGM Go" } message := fmt.Sprintf("*%s*\n\nKode OTP Registrasi Anda adalah:\n*%s*\n\nKode ini berlaku selama 5 menit. Jangan berikan kode ini kepada siapa pun.", storeName, otpCode) return postFonnte(formattedPhone, message) } func SendResetPasswordOtpWa(phone string, otpCode string) bool { if config.GlobalConfig.FonnteToken == "" { return false } formattedPhone := formatPhone(phone) storeName := config.GlobalConfig.StoreName if storeName == "" { storeName = "XOGM Go" } message := fmt.Sprintf("*%s*\n\nKode OTP Reset Password Anda adalah:\n*%s*\n\nKode ini berlaku selama 10 menit. Jangan berikan kode ini kepada siapa pun.", storeName, otpCode) return postFonnte(formattedPhone, message) } func SendApprovalWa(phone string, storeName string, email string) bool { if config.GlobalConfig.FonnteToken == "" || phone == "" { return false } formattedPhone := formatPhone(phone) message := fmt.Sprintf("✅ *SELAMAT! AKUN ANDA TELAH DISETUJUI*\n\nPerusahaan/Toko *%s* sudah diaktifkan!\nAnda dapat mencoba login ke Dashboard Kasir sekarang menggunakan email:\n_%s_\n\n*PENTING*: Agar dapat mulai menerima transaksi, pastikan Anda melakukan pengisian saldo (Top Up) Wallet/Dompet terlebih dahulu melalui menu Top Up di aplikasi.\n\nSukses Selalu,\nTim Pengelola XOGM Go.", storeName, email) return postFonnte(formattedPhone, message) } func SendNewRegistrationAdminWa(adminPhone string, storeName string, ownerName string, email string, phone string) bool { if config.GlobalConfig.FonnteToken == "" || adminPhone == "" { return false } formattedPhone := formatPhone(adminPhone) message := fmt.Sprintf("🚨 *PENDAFTARAN BARU XOGM GO* 🚨\n\nAda pendaftar baru yang menunggu persetujuan (approval) Anda:\n\n*Nama Toko / PT*: %s\n*Nama Owner*: %s\n*Email*: %s\n*No HP*: %s\n\nSilakan cek Dashboard Super Admin untuk menyetujui akun ini.", storeName, ownerName, email, phone) return postFonnte(formattedPhone, message) } func postFonnte(target string, message string) bool { // Gunakan http client bawaan Go dengan timeout 15 detik client := &http.Client{Timeout: 15 * time.Second} // Payload dikirim sebagai form url encoded data := url.Values{} data.Set("target", target) data.Set("message", message) data.Set("countryCode", "62") req, err := http.NewRequest("POST", FonnteAPIURL, bytes.NewBufferString(data.Encode())) if err != nil { log.Printf("[WHATSAPP] Gagal membuat request Fonnte: %v", err) return false } req.Header.Set("Authorization", config.GlobalConfig.FonnteToken) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := client.Do(req) if err != nil { log.Printf("[WHATSAPP] Error kirim Fonnte: %v", err) return false } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { log.Printf("[WHATSAPP] Gagal membaca response Fonnte: %v", err) return false } var responseData map[string]interface{} if err := json.Unmarshal(bodyBytes, &responseData); err != nil { log.Printf("[WHATSAPP] Gagal parse JSON Fonnte: %v", err) return false } status, ok := responseData["status"].(bool) if ok && status { log.Printf("[WHATSAPP] Notification terkirim ke target %s", maskPhone(target)) return true } reason := responseData["reason"] log.Printf("[WHATSAPP] Fonnte gagal mengirim: %v", reason) return false } func maskPhone(phone string) string { if len(phone) <= 6 { return phone } return phone[:4] + "****" + phone[len(phone)-2:] }