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>
This commit is contained in:
74
internal/notify/fcm.go
Normal file
74
internal/notify/fcm.go
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user