package repository import ( "context" "fmt" "time" "github.com/google/uuid" "gorm.io/gorm" ) // CustomerDevice is one install of the customer app that can receive push // notifications. type CustomerDevice struct { CustomerID uuid.UUID DeviceID string Platform *string FCMToken string AppVersion *string } // CustomerDeviceRepository keeps the FCM tokens of the customer app. type CustomerDeviceRepository interface { // Register saves the device's current token. A token is only ever one customer's: // registering it takes it away from whoever had it before on that phone, so they // stop getting this customer's notifications. Register(ctx context.Context, device CustomerDevice, now time.Time) error // Unregister forgets a device, for instance on logout. An unknown device is not an error. Unregister(ctx context.Context, customerID uuid.UUID, deviceID string) error ListTokens(ctx context.Context, customerID uuid.UUID) ([]string, error) } type customerDeviceRepository struct { db *gorm.DB } func NewCustomerDeviceRepository(db *gorm.DB) CustomerDeviceRepository { return &customerDeviceRepository{db: db} } func (r *customerDeviceRepository) Register(ctx context.Context, d CustomerDevice, now time.Time) error { return DBFromContext(ctx, r.db).WithContext(ctx).Transaction(func(tx *gorm.DB) error { err := tx.Exec(`DELETE FROM customer_devices WHERE fcm_token = ? AND NOT (customer_id = ? AND device_id = ?)`, d.FCMToken, d.CustomerID, d.DeviceID).Error if err != nil { return fmt.Errorf("failed to release FCM token: %w", err) } err = tx.Exec(`INSERT INTO customer_devices (customer_id, device_id, platform, fcm_token, app_version, last_active_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (customer_id, device_id) DO UPDATE SET platform = EXCLUDED.platform, fcm_token = EXCLUDED.fcm_token, app_version = EXCLUDED.app_version, last_active_at = EXCLUDED.last_active_at, updated_at = NOW()`, d.CustomerID, d.DeviceID, d.Platform, d.FCMToken, d.AppVersion, now).Error if err != nil { return fmt.Errorf("failed to register customer device: %w", err) } return nil }) } func (r *customerDeviceRepository) Unregister(ctx context.Context, customerID uuid.UUID, deviceID string) error { err := DBFromContext(ctx, r.db).WithContext(ctx). Exec(`DELETE FROM customer_devices WHERE customer_id = ? AND device_id = ?`, customerID, deviceID).Error if err != nil { return fmt.Errorf("failed to unregister customer device: %w", err) } return nil } func (r *customerDeviceRepository) ListTokens(ctx context.Context, customerID uuid.UUID) ([]string, error) { var tokens []string err := DBFromContext(ctx, r.db).WithContext(ctx). Table("customer_devices"). Where("customer_id = ? AND fcm_token <> ''", customerID). Order("last_active_at DESC NULLS LAST"). Pluck("fcm_token", &tokens).Error if err != nil { return nil, fmt.Errorf("failed to list customer FCM tokens: %w", err) } return tokens, nil }