renewal-unlock: orders.renews_at lazy expiry + owner unlock notices (ntfy/sms channels, exempt from metering) — verified
This commit is contained in:
91
frontend/internal/notify/notify.go
Normal file
91
frontend/internal/notify/notify.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Package notify sends owner notifications (unlock/renewal notices).
|
||||
// Channels: ntfy (self-hosted, default), SMS (via the sms.Sender), or both,
|
||||
// selected by NOTIFY_CHANNEL=ntfy|sms|both.
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"wherewoof/frontend/internal/sms"
|
||||
)
|
||||
|
||||
// Notifier delivers messages to the owner.
|
||||
type Notifier struct {
|
||||
sender sms.Sender // used when the SMS channel is enabled
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// New builds a Notifier. sender may be nil (SMS channel disabled).
|
||||
func New(sender sms.Sender) *Notifier {
|
||||
return &Notifier{sender: sender, client: &http.Client{Timeout: 10 * time.Second}}
|
||||
}
|
||||
|
||||
// Channel returns the configured channel: ntfy (default), sms, or both.
|
||||
func (n *Notifier) Channel() string {
|
||||
c := os.Getenv("NOTIFY_CHANNEL")
|
||||
if c != "sms" && c != "both" {
|
||||
return "ntfy"
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Send delivers the message to the owner's phone (SMS) and/or the ntfy topic.
|
||||
// Unlock notices are system messages — they are NOT subject to SMS metering.
|
||||
func (n *Notifier) Send(ownerPhone, message string) {
|
||||
ch := n.Channel()
|
||||
if ch == "sms" || ch == "both" {
|
||||
if n.sender != nil {
|
||||
if err := n.sender.Send(sms.NormalizeAU(ownerPhone), message); err != nil {
|
||||
log.Println("notify sms failed:", err)
|
||||
}
|
||||
} else {
|
||||
log.Println("notify: SMS channel selected but no sender configured")
|
||||
}
|
||||
}
|
||||
if ch == "ntfy" || ch == "both" {
|
||||
n.sendNtfy(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notifier) sendNtfy(message string) {
|
||||
url := os.Getenv("NTFY_URL")
|
||||
if url == "" {
|
||||
url = "https://ntfy.sh"
|
||||
}
|
||||
topic := os.Getenv("NTFY_TOPIC")
|
||||
if topic == "" {
|
||||
topic = "wherewoof-owner-alerts"
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url+"/"+topic, bytes.NewBufferString(message))
|
||||
if err != nil {
|
||||
log.Println("notify ntfy:", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Title", "Where Woof")
|
||||
resp, err := n.client.Do(req)
|
||||
if err != nil {
|
||||
log.Println("notify ntfy failed:", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
log.Printf("notify ntfy: status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// UnlockMessage builds the owner notice for a gated scan.
|
||||
func UnlockMessage(tagCode, reason string) string {
|
||||
switch reason {
|
||||
case "lapsed":
|
||||
return fmt.Sprintf("Where Woof: your tag %s was scanned, but your plan is lapsed. Renew to see the finder's details: https://where-woof.com/account", tagCode)
|
||||
case "credits":
|
||||
return fmt.Sprintf("Where Woof: your tag %s was scanned, but you're out of SMS credits. Top up or renew to see the finder's details: https://where-woof.com/account", tagCode)
|
||||
default:
|
||||
return fmt.Sprintf("Where Woof: your tag %s was scanned. Manage your plan: https://where-woof.com/account", tagCode)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user