Files
doormile_backend/internal/notify/fcm.go
Suriya c91c887726 Add assignment engine, FCM, WebSockets, city gate, and internal APIs
- 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>
2026-06-25 12:31:53 +05:30

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
}