package processor import ( "context" "errors" "fmt" "strings" "time" "github.com/google/uuid" "apskel-pos-be/internal/logger" "apskel-pos-be/internal/repository" ) // ErrInvalidCustomerDevice wraps every rejection of a device registration. var ErrInvalidCustomerDevice = errors.New("invalid customer device") type customerPushSender interface { SendMulticastNotification(ctx context.Context, tokens []string, title string, body string, data map[string]string) error } // CustomerDeviceProcessor keeps the customer app's FCM tokens and sends push // notifications to a customer's devices. type CustomerDeviceProcessor struct { repo repository.CustomerDeviceRepository // Nil when FCM is not configured; notifications are then skipped. fcm customerPushSender now func() time.Time } func NewCustomerDeviceProcessor(repo repository.CustomerDeviceRepository, fcm customerPushSender) *CustomerDeviceProcessor { return &CustomerDeviceProcessor{repo: repo, fcm: fcm, now: time.Now} } // Register saves the FCM token the app got for this device. The app calls it after // login and whenever FCM gives it a new token. func (p *CustomerDeviceProcessor) Register(ctx context.Context, device repository.CustomerDevice) error { device.DeviceID = strings.TrimSpace(device.DeviceID) device.FCMToken = strings.TrimSpace(device.FCMToken) switch { case device.DeviceID == "" || len(device.DeviceID) > 255: return fmt.Errorf("%w: device_id is required, at most 255 characters", ErrInvalidCustomerDevice) case device.FCMToken == "" || len(device.FCMToken) > 512: return fmt.Errorf("%w: fcm_token is required, at most 512 characters", ErrInvalidCustomerDevice) } if device.Platform != nil { platform := strings.ToLower(strings.TrimSpace(*device.Platform)) if platform != "android" && platform != "ios" && platform != "web" { return fmt.Errorf("%w: platform must be android, ios or web", ErrInvalidCustomerDevice) } device.Platform = &platform } return p.repo.Register(ctx, device, p.now()) } // Unregister forgets a device, so it stops getting the customer's notifications. func (p *CustomerDeviceProcessor) Unregister(ctx context.Context, customerID uuid.UUID, deviceID string) error { return p.repo.Unregister(ctx, customerID, strings.TrimSpace(deviceID)) } // Notify pushes a notification to every device of the customer through FCM. A // customer without a registered device gets nothing, which is not an error. func (p *CustomerDeviceProcessor) Notify(ctx context.Context, customerID uuid.UUID, title, body string, data map[string]string) error { if p.fcm == nil { logger.NonContext.Info(fmt.Sprintf("FCM is not configured; not notifying customer %s", customerID)) return nil } tokens, err := p.repo.ListTokens(ctx, customerID) if err != nil { return err } if len(tokens) == 0 { return nil } return p.fcm.SendMulticastNotification(ctx, tokens, title, body, data) }