Implement FCM
This commit is contained in:
@@ -56,6 +56,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
repos.userRepo,
|
||||
repos.sessionRepo,
|
||||
repos.orderRepo,
|
||||
processors.fcmClient,
|
||||
)
|
||||
|
||||
a.router = router.NewRouter(
|
||||
@@ -295,12 +296,14 @@ type processors struct {
|
||||
customerPointsProcessor *processor.CustomerPointsProcessor
|
||||
otpProcessor processor.OtpProcessor
|
||||
fileClient processor.FileClient
|
||||
fcmClient client.FcmClient
|
||||
inventoryMovementService service.InventoryMovementService
|
||||
}
|
||||
|
||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||
fileClient := client.NewFileClient(cfg.S3Config)
|
||||
fonnteClient := client.NewFonnteClient(cfg.GetFonnte())
|
||||
fcmClient := client.NewFcmClient(cfg.GetFirebase())
|
||||
otpProcessor := processor.NewOtpProcessor(fonnteClient, repos.otpRepo)
|
||||
inventoryMovementService := service.NewInventoryMovementService(repos.inventoryMovementRepo, repos.ingredientRepo)
|
||||
|
||||
@@ -342,6 +345,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo),
|
||||
otpProcessor: otpProcessor,
|
||||
fileClient: fileClient,
|
||||
fcmClient: fcmClient,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"apskel-pos-be/config"
|
||||
|
||||
firebase "firebase.google.com/go/v4"
|
||||
"firebase.google.com/go/v4/messaging"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
type FcmClient interface {
|
||||
SendMulticastNotification(ctx context.Context, tokens []string, title string, body string) error
|
||||
}
|
||||
|
||||
type fcmClient struct {
|
||||
messagingClient *messaging.Client
|
||||
}
|
||||
|
||||
func NewFcmClient(cfg *config.Firebase) FcmClient {
|
||||
if cfg == nil || cfg.GetCredentialsFile() == "" {
|
||||
log.Println("FCM: credentials file not configured, FCM client is disabled")
|
||||
return &fcmClient{messagingClient: nil}
|
||||
}
|
||||
|
||||
opt := option.WithCredentialsFile(cfg.GetCredentialsFile())
|
||||
app, err := firebase.NewApp(context.Background(), nil, opt)
|
||||
if err != nil {
|
||||
log.Printf("FCM: failed to initialize Firebase app: %v", err)
|
||||
return &fcmClient{messagingClient: nil}
|
||||
}
|
||||
|
||||
client, err := app.Messaging(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("FCM: failed to create messaging client: %v", err)
|
||||
return &fcmClient{messagingClient: nil}
|
||||
}
|
||||
|
||||
log.Println("FCM: client initialized successfully")
|
||||
return &fcmClient{messagingClient: client}
|
||||
}
|
||||
|
||||
func (c *fcmClient) SendMulticastNotification(ctx context.Context, tokens []string, title string, body string) error {
|
||||
if c.messagingClient == nil {
|
||||
log.Println("FCM: client not initialized, skipping notification")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
message := &messaging.MulticastMessage{
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Tokens: tokens,
|
||||
Android: &messaging.AndroidConfig{
|
||||
Priority: "high",
|
||||
},
|
||||
}
|
||||
|
||||
response, err := c.messagingClient.SendMulticast(ctx, message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("FCM: failed to send multicast notification: %w", err)
|
||||
}
|
||||
|
||||
if response.FailureCount > 0 {
|
||||
log.Printf("FCM: %d tokens failed out of %d", response.FailureCount, len(tokens))
|
||||
for i, resp := range response.Responses {
|
||||
if !resp.Success {
|
||||
log.Printf("FCM: token[%d] failed: %v", i, resp.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("FCM: sent %d/%d notifications successfully", response.SuccessCount, len(tokens))
|
||||
return nil
|
||||
}
|
||||
@@ -35,16 +35,17 @@ type UpdateUserOutletRequest struct {
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
FcmToken *string `json:"fcm_token,omitempty"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
RefreshExpiresAt time.Time `json:"refresh_expires_at"`
|
||||
User UserResponse `json:"user"`
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
RefreshExpiresAt time.Time `json:"refresh_expires_at"`
|
||||
User UserResponse `json:"user"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
|
||||
@@ -49,6 +49,7 @@ type User struct {
|
||||
Role UserRole `gorm:"not null;size:50" json:"role" validate:"required,oneof=admin manager cashier waiter"`
|
||||
Permissions Permissions `gorm:"type:jsonb;default:'{}'" json:"permissions"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
FcmToken *string `gorm:"size:512" json:"fcm_token,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/client"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"apskel-pos-be/internal/util"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -28,6 +30,7 @@ type SelfOrderHandler struct {
|
||||
userRepo processor.UserRepository
|
||||
sessionRepo repository.SessionRepository
|
||||
orderRepo repository.OrderRepository
|
||||
fcmClient client.FcmClient
|
||||
}
|
||||
|
||||
func NewSelfOrderHandler(
|
||||
@@ -39,6 +42,7 @@ func NewSelfOrderHandler(
|
||||
userRepo processor.UserRepository,
|
||||
sessionRepo repository.SessionRepository,
|
||||
orderRepo repository.OrderRepository,
|
||||
fcmClient client.FcmClient,
|
||||
) *SelfOrderHandler {
|
||||
return &SelfOrderHandler{
|
||||
orderService: orderService,
|
||||
@@ -49,6 +53,7 @@ func NewSelfOrderHandler(
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
orderRepo: orderRepo,
|
||||
fcmClient: fcmClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,10 +356,38 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
go h.sendNewOrderNotification(context.Background(), table.OrganizationID, table.TableName, len(req.OrderItems))
|
||||
|
||||
contractResp := transformer.OrderModelToContract(response)
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "SelfOrderHandler::CreateOrder")
|
||||
}
|
||||
|
||||
func (h *SelfOrderHandler) sendNewOrderNotification(ctx context.Context, organizationID uuid.UUID, tableName string, itemCount int) {
|
||||
users, err := h.userRepo.GetUsersWithFcmTokenByOrganization(ctx, organizationID)
|
||||
if err != nil {
|
||||
log.Printf("SelfOrderHandler::sendNewOrderNotification -> failed to get users with FCM token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tokens := make([]string, 0, len(users))
|
||||
for _, u := range users {
|
||||
if u.FcmToken != nil && *u.FcmToken != "" {
|
||||
tokens = append(tokens, *u.FcmToken)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
title := "Order Baru"
|
||||
body := fmt.Sprintf("Order baru dari Meja %s — %d item", tableName, itemCount)
|
||||
|
||||
if err := h.fcmClient.SendMulticastNotification(ctx, tokens, title, body); err != nil {
|
||||
log.Printf("SelfOrderHandler::sendNewOrderNotification -> failed to send FCM notification: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
sessionID := c.Param("sessionId")
|
||||
|
||||
@@ -253,3 +253,17 @@ func (p *UserProcessorImpl) UpdateUserOutlet(ctx context.Context, userID uuid.UU
|
||||
|
||||
return mappers.UserEntityToResponse(existingUser), nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) UpdateFcmToken(ctx context.Context, userID uuid.UUID, fcmToken string) error {
|
||||
_, err := p.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
err = p.userRepo.UpdateFcmToken(ctx, userID, fcmToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update FCM token: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,4 +19,6 @@ type UserRepository interface {
|
||||
UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.User, int64, error)
|
||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
UpdateFcmToken(ctx context.Context, id uuid.UUID, fcmToken string) error
|
||||
GetUsersWithFcmTokenByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error)
|
||||
}
|
||||
|
||||
@@ -110,3 +110,17 @@ func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]inter
|
||||
err := query.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) UpdateFcmToken(ctx context.Context, id uuid.UUID, fcmToken string) error {
|
||||
return r.db.WithContext(ctx).Model(&entities.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("fcm_token", fcmToken).Error
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) GetUsersWithFcmTokenByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
|
||||
var users []*entities.User
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("organization_id = ? AND is_active = ? AND fcm_token IS NOT NULL AND fcm_token != ''", organizationID, true).
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/config"
|
||||
@@ -24,11 +25,11 @@ type AuthService interface {
|
||||
}
|
||||
|
||||
type AuthServiceImpl struct {
|
||||
userProcessor UserProcessor
|
||||
jwtSecret string
|
||||
refreshSecret string
|
||||
tokenTTL time.Duration
|
||||
refreshTokenTTL time.Duration
|
||||
userProcessor UserProcessor
|
||||
jwtSecret string
|
||||
refreshSecret string
|
||||
tokenTTL time.Duration
|
||||
refreshTokenTTL time.Duration
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
@@ -81,6 +82,8 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
|
||||
return nil, fmt.Errorf("failed to generate refresh token: %w", err)
|
||||
}
|
||||
|
||||
go s.saveFcmToken(context.Background(), userResponse.ID, req.FcmToken)
|
||||
|
||||
return &contract.LoginResponse{
|
||||
Token: token,
|
||||
RefreshToken: refreshToken,
|
||||
@@ -90,6 +93,14 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) saveFcmToken(ctx context.Context, userID uuid.UUID, fcmToken *string) {
|
||||
if fcmToken != nil && *fcmToken != "" {
|
||||
if err := s.userProcessor.UpdateFcmToken(ctx, userID, *fcmToken); err != nil {
|
||||
log.Printf("failed to save FCM token for user %s: %v", userID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserResponse, error) {
|
||||
claims, err := s.parseToken(tokenString)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,4 +20,5 @@ type UserProcessor interface {
|
||||
ActivateUser(ctx context.Context, userID uuid.UUID) error
|
||||
DeactivateUser(ctx context.Context, userID uuid.UUID) error
|
||||
UpdateUserOutlet(ctx context.Context, userID uuid.UUID, req *models.UpdateUserOutletRequest) (*models.UserResponse, error)
|
||||
UpdateFcmToken(ctx context.Context, userID uuid.UUID, fcmToken string) error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user