- internal/assignment: GEORADIUS miler assignment with retry/escalation, customer-side provider scoring, FCM notifications on assign - internal/notify: Firebase Admin SDK (FCM) client initialisation - internal/ws: WebSocket handlers for live parcel tracking and customer↔miler chat - middlewares: city gate (pincode prefix validation), internal API key auth, WebSocket JWT auth - controllers: InternalNotify + InternalReassign for machine-to-machine calls; pricing helpers wired into CreateCustomerBooking and CreateCRMBooking - routes: /internal/*, /ws/bookings/:id/track, /ws/bookings/:id/chat - models/users, models/doormile_pricing: new fields for device tokens, assignment state, pricing bands - seed_data.sql: initial pricing seed rows .env and Firebase service-account JSON intentionally excluded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
1.8 KiB
Go
75 lines
1.8 KiB
Go
package notify
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
firebase "firebase.google.com/go/v4"
|
|
"firebase.google.com/go/v4/messaging"
|
|
"doormile/utils"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
var (
|
|
fcmClient *messaging.Client
|
|
fcmOnce sync.Once
|
|
)
|
|
|
|
// InitFCM initializes the Firebase Admin SDK using the service account JSON file
|
|
// at FIREBASE_SERVICE_ACCOUNT_PATH. Safe to call from main() at startup.
|
|
// If the env var is unset or the file is invalid, FCM is disabled for the process
|
|
// lifetime — all SendToDevice calls become no-ops.
|
|
func InitFCM() {
|
|
fcmOnce.Do(func() {
|
|
path := os.Getenv("FIREBASE_SERVICE_ACCOUNT_PATH")
|
|
if path == "" {
|
|
utils.Warn("FCM: FIREBASE_SERVICE_ACCOUNT_PATH not set — push notifications disabled")
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
app, err := firebase.NewApp(ctx, nil, option.WithCredentialsFile(path))
|
|
if err != nil {
|
|
utils.Error("FCM: failed to initialize Firebase app", "error", err)
|
|
return
|
|
}
|
|
|
|
client, err := app.Messaging(ctx)
|
|
if err != nil {
|
|
utils.Error("FCM: failed to get Messaging client", "error", err)
|
|
return
|
|
}
|
|
|
|
fcmClient = client
|
|
utils.Info("FCM: initialized successfully")
|
|
})
|
|
}
|
|
|
|
// SendToDevice sends an FCM notification to a single device token.
|
|
// Returns nil (no-op) if FCM was not initialized or the token is empty.
|
|
// The caller should log errors but must not block the booking flow on failure.
|
|
func SendToDevice(token, title, body string, data map[string]string) error {
|
|
if fcmClient == nil || token == "" {
|
|
return nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
msg := &messaging.Message{
|
|
Notification: &messaging.Notification{
|
|
Title: title,
|
|
Body: body,
|
|
},
|
|
Data: data,
|
|
Token: token,
|
|
}
|
|
|
|
_, err := fcmClient.Send(ctx, msg)
|
|
return err
|
|
}
|