self-order+notification #5
@ -107,6 +107,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
middleware.customerAuthMiddleware,
|
middleware.customerAuthMiddleware,
|
||||||
services.userDeviceService,
|
services.userDeviceService,
|
||||||
validators.userDeviceValidator,
|
validators.userDeviceValidator,
|
||||||
|
services.notificationService,
|
||||||
|
validators.notificationValidator,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -195,6 +197,9 @@ type repositories struct {
|
|||||||
otpRepo repository.OtpRepository
|
otpRepo repository.OtpRepository
|
||||||
txManager *repository.TxManager
|
txManager *repository.TxManager
|
||||||
userDeviceRepo *repository.UserDeviceRepositoryImpl
|
userDeviceRepo *repository.UserDeviceRepositoryImpl
|
||||||
|
notificationRepo *repository.NotificationRepositoryImpl
|
||||||
|
notificationReceiverRepo *repository.NotificationReceiverRepositoryImpl
|
||||||
|
notificationDeliveryRepo *repository.NotificationDeliveryRepositoryImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
func (a *App) initRepositories() *repositories {
|
||||||
@ -242,6 +247,9 @@ func (a *App) initRepositories() *repositories {
|
|||||||
otpRepo: repository.NewOtpRepository(a.db),
|
otpRepo: repository.NewOtpRepository(a.db),
|
||||||
txManager: repository.NewTxManager(a.db),
|
txManager: repository.NewTxManager(a.db),
|
||||||
userDeviceRepo: repository.NewUserDeviceRepositoryImpl(a.db),
|
userDeviceRepo: repository.NewUserDeviceRepositoryImpl(a.db),
|
||||||
|
notificationRepo: repository.NewNotificationRepository(a.db),
|
||||||
|
notificationReceiverRepo: repository.NewNotificationReceiverRepository(a.db),
|
||||||
|
notificationDeliveryRepo: repository.NewNotificationDeliveryRepository(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,6 +293,7 @@ type processors struct {
|
|||||||
fileClient processor.FileClient
|
fileClient processor.FileClient
|
||||||
inventoryMovementService service.InventoryMovementService
|
inventoryMovementService service.InventoryMovementService
|
||||||
userDeviceProcessor *processor.UserDeviceProcessorImpl
|
userDeviceProcessor *processor.UserDeviceProcessorImpl
|
||||||
|
notificationProcessor *processor.NotificationProcessorImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||||
@ -333,6 +342,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
fileClient: fileClient,
|
fileClient: fileClient,
|
||||||
inventoryMovementService: inventoryMovementService,
|
inventoryMovementService: inventoryMovementService,
|
||||||
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
||||||
|
notificationProcessor: buildNotificationProcessor(cfg, repos),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,6 +380,7 @@ type services struct {
|
|||||||
customerPointsService service.CustomerPointsService
|
customerPointsService service.CustomerPointsService
|
||||||
spinGameService service.SpinGameService
|
spinGameService service.SpinGameService
|
||||||
userDeviceService service.UserDeviceService
|
userDeviceService service.UserDeviceService
|
||||||
|
notificationService service.NotificationService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||||
@ -406,6 +417,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
||||||
spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager)
|
spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager)
|
||||||
userDeviceService := service.NewUserDeviceService(processors.userDeviceProcessor)
|
userDeviceService := service.NewUserDeviceService(processors.userDeviceProcessor)
|
||||||
|
notificationService := service.NewNotificationService(processors.notificationProcessor)
|
||||||
|
|
||||||
// Update order service with order ingredient transaction service
|
// Update order service with order ingredient transaction service
|
||||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
||||||
@ -444,6 +456,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
customerPointsService: customerPointsService,
|
customerPointsService: customerPointsService,
|
||||||
spinGameService: spinGameService,
|
spinGameService: spinGameService,
|
||||||
userDeviceService: userDeviceService,
|
userDeviceService: userDeviceService,
|
||||||
|
notificationService: notificationService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,6 +497,7 @@ type validators struct {
|
|||||||
campaignValidator validator.CampaignValidator
|
campaignValidator validator.CampaignValidator
|
||||||
customerAuthValidator validator.CustomerAuthValidator
|
customerAuthValidator validator.CustomerAuthValidator
|
||||||
userDeviceValidator *validator.UserDeviceValidatorImpl
|
userDeviceValidator *validator.UserDeviceValidatorImpl
|
||||||
|
notificationValidator *validator.NotificationValidatorImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initValidators() *validators {
|
func (a *App) initValidators() *validators {
|
||||||
@ -512,5 +526,29 @@ func (a *App) initValidators() *validators {
|
|||||||
campaignValidator: validator.NewCampaignValidator(),
|
campaignValidator: validator.NewCampaignValidator(),
|
||||||
customerAuthValidator: validator.NewCustomerAuthValidator(),
|
customerAuthValidator: validator.NewCustomerAuthValidator(),
|
||||||
userDeviceValidator: validator.NewUserDeviceValidator(),
|
userDeviceValidator: validator.NewUserDeviceValidator(),
|
||||||
|
notificationValidator: validator.NewNotificationValidator(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildNotificationProcessor creates the notification processor with FCM integration.
|
||||||
|
// If FCM is not configured, it returns a processor with a nil FCM client (FCM dispatch will be skipped).
|
||||||
|
func buildNotificationProcessor(cfg *config.Config, repos *repositories) *processor.NotificationProcessorImpl {
|
||||||
|
var fcmClient client.FCMClient
|
||||||
|
if cfg.FCM.CredentialsFile != "" {
|
||||||
|
var err error
|
||||||
|
fcmClient, err = client.NewFCMClient(&cfg.FCM)
|
||||||
|
if err != nil {
|
||||||
|
// FCM init failure is non-fatal; notifications will still be persisted.
|
||||||
|
fcmClient = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processor.NewNotificationProcessor(
|
||||||
|
repos.notificationRepo,
|
||||||
|
repos.notificationReceiverRepo,
|
||||||
|
repos.notificationDeliveryRepo,
|
||||||
|
repos.userDeviceRepo,
|
||||||
|
repos.userRepo,
|
||||||
|
fcmClient,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -44,19 +44,21 @@ const (
|
|||||||
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
||||||
TableEntity = "table"
|
TableEntity = "table"
|
||||||
// Gamification entities
|
// Gamification entities
|
||||||
CustomerPointsEntity = "customer_points"
|
CustomerPointsEntity = "customer_points"
|
||||||
CustomerTokensEntity = "customer_tokens"
|
CustomerTokensEntity = "customer_tokens"
|
||||||
TierEntity = "tier"
|
TierEntity = "tier"
|
||||||
GameEntity = "game"
|
GameEntity = "game"
|
||||||
GamePrizeEntity = "game_prize"
|
GamePrizeEntity = "game_prize"
|
||||||
GamePlayEntity = "game_play"
|
GamePlayEntity = "game_play"
|
||||||
OmsetTrackerEntity = "omset_tracker"
|
OmsetTrackerEntity = "omset_tracker"
|
||||||
RewardEntity = "reward"
|
RewardEntity = "reward"
|
||||||
CampaignEntity = "campaign"
|
CampaignEntity = "campaign"
|
||||||
CampaignRuleEntity = "campaign_rule"
|
CampaignRuleEntity = "campaign_rule"
|
||||||
CustomerEntity = "customer"
|
CustomerEntity = "customer"
|
||||||
SpinGameHandlerEntity = "spin_game_handler"
|
SpinGameHandlerEntity = "spin_game_handler"
|
||||||
UserDeviceServiceEntity = "user_device_service"
|
UserDeviceServiceEntity = "user_device_service"
|
||||||
|
NotificationServiceEntity = "notification_service"
|
||||||
|
NotificationHandlerEntity = "notification_handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
var HttpErrorMap = map[string]int{
|
var HttpErrorMap = map[string]int{
|
||||||
|
|||||||
92
internal/contract/notification_contract.go
Normal file
92
internal/contract/notification_contract.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Request contracts ----
|
||||||
|
|
||||||
|
type SendNotificationRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||||
|
Body string `json:"body" validate:"required"`
|
||||||
|
Type string `json:"type,omitempty" validate:"omitempty,max=100"`
|
||||||
|
Category string `json:"category,omitempty" validate:"omitempty,max=100"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority,omitempty" validate:"omitempty,oneof=low normal high"`
|
||||||
|
ImageURL string `json:"image_url,omitempty" validate:"omitempty,max=512"`
|
||||||
|
ActionURL string `json:"action_url,omitempty" validate:"omitempty,max=512"`
|
||||||
|
NotifiableType string `json:"notifiable_type,omitempty" validate:"omitempty,max=100"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
ReceiverIDs []uuid.UUID `json:"receiver_ids" validate:"required,min=1"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BroadcastNotificationRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||||
|
Body string `json:"body" validate:"required"`
|
||||||
|
Type string `json:"type,omitempty" validate:"omitempty,max=100"`
|
||||||
|
Category string `json:"category,omitempty" validate:"omitempty,max=100"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority,omitempty" validate:"omitempty,oneof=low normal high"`
|
||||||
|
ImageURL string `json:"image_url,omitempty" validate:"omitempty,max=512"`
|
||||||
|
ActionURL string `json:"action_url,omitempty" validate:"omitempty,max=512"`
|
||||||
|
NotifiableType string `json:"notifiable_type,omitempty" validate:"omitempty,max=100"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNotificationsRequest struct {
|
||||||
|
Page int `form:"page" validate:"min=1"`
|
||||||
|
Limit int `form:"limit" validate:"min=1,max=100"`
|
||||||
|
IsRead *bool `form:"is_read"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Response contracts ----
|
||||||
|
|
||||||
|
type NotificationResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionURL string `json:"action_url"`
|
||||||
|
NotifiableType string `json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||||
|
SentAt *time.Time `json:"sent_at"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationReceiverResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
NotificationID uuid.UUID `json:"notification_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
IsRead bool `json:"is_read"`
|
||||||
|
ReadAt *time.Time `json:"read_at"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
|
DeletedAt *time.Time `json:"deleted_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Notification *NotificationResponse `json:"notification,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNotificationsResponse struct {
|
||||||
|
Notifications []*NotificationReceiverResponse `json:"notifications"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
UnreadCount int64 `json:"unread_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
150
internal/entities/notification.go
Normal file
150
internal/entities/notification.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationPriority string
|
||||||
|
type NotificationDeliveryStatus string
|
||||||
|
type NotificationChannel string
|
||||||
|
type NotificationProvider string
|
||||||
|
|
||||||
|
const (
|
||||||
|
NotificationPriorityLow NotificationPriority = "low"
|
||||||
|
NotificationPriorityNormal NotificationPriority = "normal"
|
||||||
|
NotificationPriorityHigh NotificationPriority = "high"
|
||||||
|
|
||||||
|
NotificationDeliveryStatusPending NotificationDeliveryStatus = "pending"
|
||||||
|
NotificationDeliveryStatusSent NotificationDeliveryStatus = "sent"
|
||||||
|
NotificationDeliveryStatusDelivered NotificationDeliveryStatus = "delivered"
|
||||||
|
NotificationDeliveryStatusFailed NotificationDeliveryStatus = "failed"
|
||||||
|
|
||||||
|
NotificationChannelPush NotificationChannel = "push"
|
||||||
|
NotificationChannelWebsocket NotificationChannel = "websocket"
|
||||||
|
NotificationChannelEmail NotificationChannel = "email"
|
||||||
|
|
||||||
|
NotificationProviderFirebase NotificationProvider = "firebase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationData is a JSON-serializable map for extra notification payload.
|
||||||
|
type NotificationData map[string]interface{}
|
||||||
|
|
||||||
|
func (d NotificationData) Value() (driver.Value, error) {
|
||||||
|
if d == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return json.Marshal(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *NotificationData) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*d = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("type assertion to []byte failed")
|
||||||
|
}
|
||||||
|
return json.Unmarshal(bytes, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification is the master notification record.
|
||||||
|
type Notification struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
Title string `gorm:"not null;size:255" json:"title"`
|
||||||
|
Body string `gorm:"type:text" json:"body"`
|
||||||
|
Type string `gorm:"size:100" json:"type"`
|
||||||
|
Category string `gorm:"size:100" json:"category"`
|
||||||
|
Priority NotificationPriority `gorm:"size:50;default:'normal'" json:"priority"`
|
||||||
|
ImageURL string `gorm:"size:512" json:"image_url"`
|
||||||
|
ActionURL string `gorm:"size:512" json:"action_url"`
|
||||||
|
NotifiableType string `gorm:"size:100" json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `gorm:"type:uuid" json:"notifiable_id"`
|
||||||
|
Data NotificationData `gorm:"type:jsonb" json:"data"`
|
||||||
|
ScheduledAt *time.Time `gorm:"type:timestamptz" json:"scheduled_at"`
|
||||||
|
SentAt *time.Time `gorm:"type:timestamptz" json:"sent_at"`
|
||||||
|
ExpiredAt *time.Time `gorm:"type:timestamptz" json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `gorm:"type:uuid" json:"created_by"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
|
||||||
|
Receivers []*NotificationReceiver `gorm:"foreignKey:NotificationID" json:"receivers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notification) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if n.ID == uuid.Nil {
|
||||||
|
n.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Notification) TableName() string {
|
||||||
|
return "notifications"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationReceiver links a notification to a specific user.
|
||||||
|
type NotificationReceiver struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
NotificationID uuid.UUID `gorm:"type:uuid;not null;index" json:"notification_id"`
|
||||||
|
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"`
|
||||||
|
IsRead bool `gorm:"default:false" json:"is_read"`
|
||||||
|
ReadAt *time.Time `gorm:"type:timestamptz" json:"read_at"`
|
||||||
|
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
|
||||||
|
DeletedAt *time.Time `gorm:"type:timestamptz" json:"deleted_at"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Notification *Notification `gorm:"foreignKey:NotificationID" json:"notification,omitempty"`
|
||||||
|
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
Deliveries []*NotificationDelivery `gorm:"foreignKey:NotificationReceiverID" json:"deliveries,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NotificationReceiver) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if n.ID == uuid.Nil {
|
||||||
|
n.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (NotificationReceiver) TableName() string {
|
||||||
|
return "notification_receivers"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationDelivery tracks per-device delivery attempts.
|
||||||
|
type NotificationDelivery struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
NotificationReceiverID uuid.UUID `gorm:"type:uuid;not null;index" json:"notification_receiver_id"`
|
||||||
|
UserDeviceID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_device_id"`
|
||||||
|
Channel NotificationChannel `gorm:"size:50;default:'push'" json:"channel"`
|
||||||
|
DeliveryStatus NotificationDeliveryStatus `gorm:"size:50;default:'pending'" json:"delivery_status"`
|
||||||
|
Provider NotificationProvider `gorm:"size:50" json:"provider"`
|
||||||
|
ProviderMessageID string `gorm:"size:255" json:"provider_message_id"`
|
||||||
|
SentAt *time.Time `gorm:"type:timestamptz" json:"sent_at"`
|
||||||
|
DeliveredAt *time.Time `gorm:"type:timestamptz" json:"delivered_at"`
|
||||||
|
FailedAt *time.Time `gorm:"type:timestamptz" json:"failed_at"`
|
||||||
|
FailureReason string `gorm:"type:text" json:"failure_reason"`
|
||||||
|
RetryCount int `gorm:"default:0" json:"retry_count"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
NotificationReceiver *NotificationReceiver `gorm:"foreignKey:NotificationReceiverID" json:"notification_receiver,omitempty"`
|
||||||
|
UserDevice *UserDevice `gorm:"foreignKey:UserDeviceID" json:"user_device,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NotificationDelivery) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if n.ID == uuid.Nil {
|
||||||
|
n.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (NotificationDelivery) TableName() string {
|
||||||
|
return "notification_deliveries"
|
||||||
|
}
|
||||||
190
internal/handler/notification_handler.go
Normal file
190
internal/handler/notification_handler.go
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationHandler struct {
|
||||||
|
notificationService service.NotificationService
|
||||||
|
notificationValidator validator.NotificationValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationHandler(
|
||||||
|
notificationService service.NotificationService,
|
||||||
|
notificationValidator validator.NotificationValidator,
|
||||||
|
) *NotificationHandler {
|
||||||
|
return &NotificationHandler{
|
||||||
|
notificationService: notificationService,
|
||||||
|
notificationValidator: notificationValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send godoc
|
||||||
|
// POST /api/v1/notifications/send
|
||||||
|
// Sends a notification to specific users.
|
||||||
|
func (h *NotificationHandler) Send(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.SendNotificationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Send -> request binding failed")
|
||||||
|
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr, errCode := h.notificationValidator.ValidateSendRequest(&req); validationErr != nil {
|
||||||
|
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::Send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.Send(ctx, &req, contextInfo.UserID)
|
||||||
|
if resp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Send -> service error")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Send")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast godoc
|
||||||
|
// POST /api/v1/notifications/broadcast
|
||||||
|
// Sends a notification to all active users in the caller's organization.
|
||||||
|
func (h *NotificationHandler) Broadcast(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.BroadcastNotificationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Broadcast -> request binding failed")
|
||||||
|
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Broadcast")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr, errCode := h.notificationValidator.ValidateBroadcastRequest(&req); validationErr != nil {
|
||||||
|
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::Broadcast")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.Broadcast(ctx, &req, contextInfo.OrganizationID, contextInfo.UserID)
|
||||||
|
if resp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Broadcast -> service error")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Broadcast")
|
||||||
|
}
|
||||||
|
|
||||||
|
// List godoc
|
||||||
|
// GET /api/v1/notifications
|
||||||
|
// Returns paginated notifications for the authenticated user.
|
||||||
|
func (h *NotificationHandler) List(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
req := contract.ListNotificationsRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::List -> query binding failed")
|
||||||
|
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::List")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr, errCode := h.notificationValidator.ValidateListRequest(&req); validationErr != nil {
|
||||||
|
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::List")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.ListForUser(ctx, &req, contextInfo.UserID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::List")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID godoc
|
||||||
|
// GET /api/v1/notifications/:id
|
||||||
|
func (h *NotificationHandler) GetByID(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::GetByID -> invalid notification ID")
|
||||||
|
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::GetByID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.GetByID(ctx, id)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::GetByID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAsRead godoc
|
||||||
|
// PUT /api/v1/notifications/:id/read
|
||||||
|
func (h *NotificationHandler) MarkAsRead(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
receiverID, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::MarkAsRead -> invalid receiver ID")
|
||||||
|
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification receiver ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::MarkAsRead")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.MarkAsRead(ctx, receiverID, contextInfo.UserID)
|
||||||
|
if resp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::MarkAsRead -> service error")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::MarkAsRead")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAllAsRead godoc
|
||||||
|
// PUT /api/v1/notifications/read-all
|
||||||
|
func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
resp := h.notificationService.MarkAllAsRead(ctx, contextInfo.UserID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::MarkAllAsRead")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete godoc
|
||||||
|
// DELETE /api/v1/notifications/:id
|
||||||
|
func (h *NotificationHandler) Delete(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
receiverID, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Delete -> invalid receiver ID")
|
||||||
|
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification receiver ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.DeleteForUser(ctx, receiverID, contextInfo.UserID)
|
||||||
|
if resp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Delete -> service error")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Delete")
|
||||||
|
}
|
||||||
85
internal/mappers/notification_mapper.go
Normal file
85
internal/mappers/notification_mapper.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NotificationEntityToResponse(e *entities.Notification) *models.NotificationResponse {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &models.NotificationResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
Title: e.Title,
|
||||||
|
Body: e.Body,
|
||||||
|
Type: e.Type,
|
||||||
|
Category: e.Category,
|
||||||
|
Priority: e.Priority,
|
||||||
|
ImageURL: e.ImageURL,
|
||||||
|
ActionURL: e.ActionURL,
|
||||||
|
NotifiableType: e.NotifiableType,
|
||||||
|
NotifiableID: e.NotifiableID,
|
||||||
|
Data: e.Data,
|
||||||
|
ScheduledAt: e.ScheduledAt,
|
||||||
|
SentAt: e.SentAt,
|
||||||
|
ExpiredAt: e.ExpiredAt,
|
||||||
|
CreatedBy: e.CreatedBy,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotificationReceiverEntityToResponse(e *entities.NotificationReceiver) *models.NotificationReceiverResponse {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp := &models.NotificationReceiverResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
NotificationID: e.NotificationID,
|
||||||
|
UserID: e.UserID,
|
||||||
|
IsRead: e.IsRead,
|
||||||
|
ReadAt: e.ReadAt,
|
||||||
|
IsDeleted: e.IsDeleted,
|
||||||
|
DeletedAt: e.DeletedAt,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
if e.Notification != nil {
|
||||||
|
resp.Notification = NotificationEntityToResponse(e.Notification)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotificationReceiverEntitiesToResponses(entities []*entities.NotificationReceiver) []*models.NotificationReceiverResponse {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
responses := make([]*models.NotificationReceiverResponse, len(entities))
|
||||||
|
for i, e := range entities {
|
||||||
|
responses[i] = NotificationReceiverEntityToResponse(e)
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotificationDeliveryEntityToResponse(e *entities.NotificationDelivery) *models.NotificationDeliveryResponse {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &models.NotificationDeliveryResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
NotificationReceiverID: e.NotificationReceiverID,
|
||||||
|
UserDeviceID: e.UserDeviceID,
|
||||||
|
Channel: e.Channel,
|
||||||
|
DeliveryStatus: e.DeliveryStatus,
|
||||||
|
Provider: e.Provider,
|
||||||
|
ProviderMessageID: e.ProviderMessageID,
|
||||||
|
SentAt: e.SentAt,
|
||||||
|
DeliveredAt: e.DeliveredAt,
|
||||||
|
FailedAt: e.FailedAt,
|
||||||
|
FailureReason: e.FailureReason,
|
||||||
|
RetryCount: e.RetryCount,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/models/notification.go
Normal file
118
internal/models/notification.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Request models ----
|
||||||
|
|
||||||
|
type SendNotificationRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionURL string `json:"action_url"`
|
||||||
|
NotifiableType string `json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
ReceiverIDs []uuid.UUID `json:"receiver_ids"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BroadcastNotificationRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionURL string `json:"action_url"`
|
||||||
|
NotifiableType string `json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarkNotificationReadRequest struct {
|
||||||
|
NotificationReceiverID uuid.UUID `json:"notification_receiver_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNotificationsRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
IsRead *bool `json:"is_read"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Response models ----
|
||||||
|
|
||||||
|
type NotificationResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionURL string `json:"action_url"`
|
||||||
|
NotifiableType string `json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||||
|
SentAt *time.Time `json:"sent_at"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationReceiverResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
NotificationID uuid.UUID `json:"notification_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
IsRead bool `json:"is_read"`
|
||||||
|
ReadAt *time.Time `json:"read_at"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
|
DeletedAt *time.Time `json:"deleted_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Notification *NotificationResponse `json:"notification,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationDeliveryResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
NotificationReceiverID uuid.UUID `json:"notification_receiver_id"`
|
||||||
|
UserDeviceID uuid.UUID `json:"user_device_id"`
|
||||||
|
Channel entities.NotificationChannel `json:"channel"`
|
||||||
|
DeliveryStatus entities.NotificationDeliveryStatus `json:"delivery_status"`
|
||||||
|
Provider entities.NotificationProvider `json:"provider"`
|
||||||
|
ProviderMessageID string `json:"provider_message_id"`
|
||||||
|
SentAt *time.Time `json:"sent_at"`
|
||||||
|
DeliveredAt *time.Time `json:"delivered_at"`
|
||||||
|
FailedAt *time.Time `json:"failed_at"`
|
||||||
|
FailureReason string `json:"failure_reason"`
|
||||||
|
RetryCount int `json:"retry_count"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNotificationsResponse struct {
|
||||||
|
Notifications []*NotificationReceiverResponse `json:"notifications"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
UnreadCount int `json:"unread_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
338
internal/processor/notification_processor.go
Normal file
338
internal/processor/notification_processor.go
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/client"
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationRepository is the interface the processor depends on.
|
||||||
|
type NotificationRepository interface {
|
||||||
|
Create(ctx context.Context, notification *entities.Notification) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*entities.Notification, error)
|
||||||
|
Update(ctx context.Context, notification *entities.Notification) error
|
||||||
|
Delete(ctx context.Context, id uuid.UUID) error
|
||||||
|
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Notification, int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationReceiverRepository is the interface the processor depends on.
|
||||||
|
type NotificationReceiverRepository interface {
|
||||||
|
Create(ctx context.Context, receiver *entities.NotificationReceiver) error
|
||||||
|
BulkCreate(ctx context.Context, receivers []*entities.NotificationReceiver) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*entities.NotificationReceiver, error)
|
||||||
|
GetByNotificationAndUser(ctx context.Context, notificationID, userID uuid.UUID) (*entities.NotificationReceiver, error)
|
||||||
|
Update(ctx context.Context, receiver *entities.NotificationReceiver) error
|
||||||
|
ListByUserID(ctx context.Context, userID uuid.UUID, isRead *bool, limit, offset int) ([]*entities.NotificationReceiver, int64, error)
|
||||||
|
CountUnreadByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
|
||||||
|
SoftDeleteByID(ctx context.Context, id uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationDeliveryRepository is the interface the processor depends on.
|
||||||
|
type NotificationDeliveryRepository interface {
|
||||||
|
Create(ctx context.Context, delivery *entities.NotificationDelivery) error
|
||||||
|
BulkCreate(ctx context.Context, deliveries []*entities.NotificationDelivery) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*entities.NotificationDelivery, error)
|
||||||
|
Update(ctx context.Context, delivery *entities.NotificationDelivery) error
|
||||||
|
ListByReceiverID(ctx context.Context, receiverID uuid.UUID) ([]*entities.NotificationDelivery, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationUserRepository is a minimal interface to fetch user devices.
|
||||||
|
type NotificationUserDeviceRepository interface {
|
||||||
|
GetByUserID(ctx context.Context, userID uuid.UUID) ([]*entities.UserDevice, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationUserRepository is a minimal interface to fetch users by org.
|
||||||
|
type NotificationUserRepository interface {
|
||||||
|
GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationProcessor defines the business logic interface.
|
||||||
|
type NotificationProcessor interface {
|
||||||
|
Send(ctx context.Context, req *models.SendNotificationRequest) (*models.NotificationResponse, error)
|
||||||
|
Broadcast(ctx context.Context, req *models.BroadcastNotificationRequest) (*models.NotificationResponse, error)
|
||||||
|
MarkAsRead(ctx context.Context, receiverID, userID uuid.UUID) (*models.NotificationReceiverResponse, error)
|
||||||
|
MarkAllAsRead(ctx context.Context, userID uuid.UUID) error
|
||||||
|
DeleteForUser(ctx context.Context, receiverID, userID uuid.UUID) error
|
||||||
|
ListForUser(ctx context.Context, req *models.ListNotificationsRequest) ([]*models.NotificationReceiverResponse, int64, int64, error)
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*models.NotificationResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationProcessorImpl struct {
|
||||||
|
notificationRepo NotificationRepository
|
||||||
|
receiverRepo NotificationReceiverRepository
|
||||||
|
deliveryRepo NotificationDeliveryRepository
|
||||||
|
userDeviceRepo NotificationUserDeviceRepository
|
||||||
|
userRepo NotificationUserRepository
|
||||||
|
fcmClient client.FCMClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationProcessor(
|
||||||
|
notificationRepo NotificationRepository,
|
||||||
|
receiverRepo NotificationReceiverRepository,
|
||||||
|
deliveryRepo NotificationDeliveryRepository,
|
||||||
|
userDeviceRepo NotificationUserDeviceRepository,
|
||||||
|
userRepo NotificationUserRepository,
|
||||||
|
fcmClient client.FCMClient,
|
||||||
|
) *NotificationProcessorImpl {
|
||||||
|
return &NotificationProcessorImpl{
|
||||||
|
notificationRepo: notificationRepo,
|
||||||
|
receiverRepo: receiverRepo,
|
||||||
|
deliveryRepo: deliveryRepo,
|
||||||
|
userDeviceRepo: userDeviceRepo,
|
||||||
|
userRepo: userRepo,
|
||||||
|
fcmClient: fcmClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send creates a notification and dispatches it to the given receiver user IDs via FCM.
|
||||||
|
func (p *NotificationProcessorImpl) Send(ctx context.Context, req *models.SendNotificationRequest) (*models.NotificationResponse, error) {
|
||||||
|
if len(req.ReceiverIDs) == 0 {
|
||||||
|
return nil, fmt.Errorf("at least one receiver_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
notification := &entities.Notification{
|
||||||
|
Title: req.Title,
|
||||||
|
Body: req.Body,
|
||||||
|
Type: req.Type,
|
||||||
|
Category: req.Category,
|
||||||
|
Priority: req.Priority,
|
||||||
|
ImageURL: req.ImageURL,
|
||||||
|
ActionURL: req.ActionURL,
|
||||||
|
NotifiableType: req.NotifiableType,
|
||||||
|
NotifiableID: req.NotifiableID,
|
||||||
|
Data: req.Data,
|
||||||
|
ScheduledAt: req.ScheduledAt,
|
||||||
|
ExpiredAt: req.ExpiredAt,
|
||||||
|
CreatedBy: req.CreatedBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.notificationRepo.Create(ctx, notification); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create receiver records and dispatch FCM per user.
|
||||||
|
for _, userID := range req.ReceiverIDs {
|
||||||
|
receiver := &entities.NotificationReceiver{
|
||||||
|
NotificationID: notification.ID,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
if err := p.receiverRepo.Create(ctx, receiver); err != nil {
|
||||||
|
// Log but continue for other receivers.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
p.dispatchFCMToUser(ctx, receiver, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark notification as sent.
|
||||||
|
now := time.Now()
|
||||||
|
notification.SentAt = &now
|
||||||
|
_ = p.notificationRepo.Update(ctx, notification)
|
||||||
|
|
||||||
|
return mappers.NotificationEntityToResponse(notification), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast sends a notification to all active users in an organization.
|
||||||
|
func (p *NotificationProcessorImpl) Broadcast(ctx context.Context, req *models.BroadcastNotificationRequest) (*models.NotificationResponse, error) {
|
||||||
|
users, err := p.userRepo.GetActiveUsers(ctx, req.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch organization users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
notification := &entities.Notification{
|
||||||
|
Title: req.Title,
|
||||||
|
Body: req.Body,
|
||||||
|
Type: req.Type,
|
||||||
|
Category: req.Category,
|
||||||
|
Priority: req.Priority,
|
||||||
|
ImageURL: req.ImageURL,
|
||||||
|
ActionURL: req.ActionURL,
|
||||||
|
NotifiableType: req.NotifiableType,
|
||||||
|
NotifiableID: req.NotifiableID,
|
||||||
|
Data: req.Data,
|
||||||
|
ScheduledAt: req.ScheduledAt,
|
||||||
|
ExpiredAt: req.ExpiredAt,
|
||||||
|
CreatedBy: req.CreatedBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.notificationRepo.Create(ctx, notification); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build receiver records in bulk.
|
||||||
|
receivers := make([]*entities.NotificationReceiver, 0, len(users))
|
||||||
|
for _, u := range users {
|
||||||
|
receivers = append(receivers, &entities.NotificationReceiver{
|
||||||
|
NotificationID: notification.ID,
|
||||||
|
UserID: u.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.receiverRepo.BulkCreate(ctx, receivers); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create notification receivers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch FCM for each receiver.
|
||||||
|
for _, receiver := range receivers {
|
||||||
|
p.dispatchFCMToUser(ctx, receiver, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
notification.SentAt = &now
|
||||||
|
_ = p.notificationRepo.Update(ctx, notification)
|
||||||
|
|
||||||
|
return mappers.NotificationEntityToResponse(notification), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAsRead marks a single notification receiver record as read.
|
||||||
|
func (p *NotificationProcessorImpl) MarkAsRead(ctx context.Context, receiverID, userID uuid.UUID) (*models.NotificationReceiverResponse, error) {
|
||||||
|
receiver, err := p.receiverRepo.GetByID(ctx, receiverID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("notification not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if receiver.UserID != userID {
|
||||||
|
return nil, fmt.Errorf("unauthorized: notification does not belong to user")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !receiver.IsRead {
|
||||||
|
now := time.Now()
|
||||||
|
receiver.IsRead = true
|
||||||
|
receiver.ReadAt = &now
|
||||||
|
if err := p.receiverRepo.Update(ctx, receiver); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to mark notification as read: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.NotificationReceiverEntityToResponse(receiver), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAllAsRead marks all unread notifications for a user as read.
|
||||||
|
func (p *NotificationProcessorImpl) MarkAllAsRead(ctx context.Context, userID uuid.UUID) error {
|
||||||
|
isRead := false
|
||||||
|
receivers, _, err := p.receiverRepo.ListByUserID(ctx, userID, &isRead, 1000, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch unread notifications: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, r := range receivers {
|
||||||
|
r.IsRead = true
|
||||||
|
r.ReadAt = &now
|
||||||
|
_ = p.receiverRepo.Update(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteForUser soft-deletes a notification receiver record for a user.
|
||||||
|
func (p *NotificationProcessorImpl) DeleteForUser(ctx context.Context, receiverID, userID uuid.UUID) error {
|
||||||
|
receiver, err := p.receiverRepo.GetByID(ctx, receiverID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("notification not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if receiver.UserID != userID {
|
||||||
|
return fmt.Errorf("unauthorized: notification does not belong to user")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.receiverRepo.SoftDeleteByID(ctx, receiverID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListForUser returns paginated notifications for a user.
|
||||||
|
// Returns: receivers, total, unreadCount, error
|
||||||
|
func (p *NotificationProcessorImpl) ListForUser(ctx context.Context, req *models.ListNotificationsRequest) ([]*models.NotificationReceiverResponse, int64, int64, error) {
|
||||||
|
offset := (req.Page - 1) * req.Limit
|
||||||
|
|
||||||
|
receivers, total, err := p.receiverRepo.ListByUserID(ctx, req.UserID, req.IsRead, req.Limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, fmt.Errorf("failed to list notifications: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unreadCount, err := p.receiverRepo.CountUnreadByUserID(ctx, req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
unreadCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := mappers.NotificationReceiverEntitiesToResponses(receivers)
|
||||||
|
return responses, total, unreadCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID returns a single notification by its ID.
|
||||||
|
func (p *NotificationProcessorImpl) GetByID(ctx context.Context, id uuid.UUID) (*models.NotificationResponse, error) {
|
||||||
|
notification, err := p.notificationRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("notification not found: %w", err)
|
||||||
|
}
|
||||||
|
return mappers.NotificationEntityToResponse(notification), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchFCMToUser fetches all FCM tokens for a user and sends the push notification.
|
||||||
|
func (p *NotificationProcessorImpl) dispatchFCMToUser(ctx context.Context, receiver *entities.NotificationReceiver, notification *entities.Notification) {
|
||||||
|
if p.fcmClient == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := p.userDeviceRepo.GetByUserID(ctx, receiver.UserID)
|
||||||
|
if err != nil || len(devices) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build FCM data payload.
|
||||||
|
data := map[string]string{
|
||||||
|
"notification_id": notification.ID.String(),
|
||||||
|
"notification_receiver_id": receiver.ID.String(),
|
||||||
|
"type": notification.Type,
|
||||||
|
"category": notification.Category,
|
||||||
|
"action_url": notification.ActionURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect valid FCM tokens and create delivery records.
|
||||||
|
tokens := make([]string, 0, len(devices))
|
||||||
|
deliveries := make([]*entities.NotificationDelivery, 0, len(devices))
|
||||||
|
|
||||||
|
for _, device := range devices {
|
||||||
|
if device.FCMToken == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokens = append(tokens, device.FCMToken)
|
||||||
|
deliveries = append(deliveries, &entities.NotificationDelivery{
|
||||||
|
NotificationReceiverID: receiver.ID,
|
||||||
|
UserDeviceID: device.ID,
|
||||||
|
Channel: entities.NotificationChannelPush,
|
||||||
|
DeliveryStatus: entities.NotificationDeliveryStatusPending,
|
||||||
|
Provider: entities.NotificationProviderFirebase,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist delivery records before sending.
|
||||||
|
_ = p.deliveryRepo.BulkCreate(ctx, deliveries)
|
||||||
|
|
||||||
|
// Send via FCM multicast.
|
||||||
|
now := time.Now()
|
||||||
|
sendErr := p.fcmClient.SendMulticastNotification(ctx, tokens, notification.Title, notification.Body, data)
|
||||||
|
|
||||||
|
// Update delivery status.
|
||||||
|
for _, delivery := range deliveries {
|
||||||
|
if sendErr != nil {
|
||||||
|
delivery.DeliveryStatus = entities.NotificationDeliveryStatusFailed
|
||||||
|
delivery.FailedAt = &now
|
||||||
|
delivery.FailureReason = sendErr.Error()
|
||||||
|
} else {
|
||||||
|
delivery.DeliveryStatus = entities.NotificationDeliveryStatusSent
|
||||||
|
delivery.SentAt = &now
|
||||||
|
}
|
||||||
|
_ = p.deliveryRepo.Update(ctx, delivery)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
internal/repository/notification_delivery_repository.go
Normal file
59
internal/repository/notification_delivery_repository.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationDeliveryRepository interface {
|
||||||
|
Create(ctx context.Context, delivery *entities.NotificationDelivery) error
|
||||||
|
BulkCreate(ctx context.Context, deliveries []*entities.NotificationDelivery) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*entities.NotificationDelivery, error)
|
||||||
|
Update(ctx context.Context, delivery *entities.NotificationDelivery) error
|
||||||
|
ListByReceiverID(ctx context.Context, receiverID uuid.UUID) ([]*entities.NotificationDelivery, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationDeliveryRepositoryImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationDeliveryRepository(db *gorm.DB) *NotificationDeliveryRepositoryImpl {
|
||||||
|
return &NotificationDeliveryRepositoryImpl{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationDeliveryRepositoryImpl) Create(ctx context.Context, delivery *entities.NotificationDelivery) error {
|
||||||
|
return r.db.WithContext(ctx).Create(delivery).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationDeliveryRepositoryImpl) BulkCreate(ctx context.Context, deliveries []*entities.NotificationDelivery) error {
|
||||||
|
if len(deliveries) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Create(&deliveries).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationDeliveryRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.NotificationDelivery, error) {
|
||||||
|
var delivery entities.NotificationDelivery
|
||||||
|
err := r.db.WithContext(ctx).First(&delivery, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &delivery, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationDeliveryRepositoryImpl) Update(ctx context.Context, delivery *entities.NotificationDelivery) error {
|
||||||
|
return r.db.WithContext(ctx).Save(delivery).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationDeliveryRepositoryImpl) ListByReceiverID(ctx context.Context, receiverID uuid.UUID) ([]*entities.NotificationDelivery, error) {
|
||||||
|
var deliveries []*entities.NotificationDelivery
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("notification_receiver_id = ?", receiverID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&deliveries).Error
|
||||||
|
return deliveries, err
|
||||||
|
}
|
||||||
108
internal/repository/notification_receiver_repository.go
Normal file
108
internal/repository/notification_receiver_repository.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationReceiverRepository interface {
|
||||||
|
Create(ctx context.Context, receiver *entities.NotificationReceiver) error
|
||||||
|
BulkCreate(ctx context.Context, receivers []*entities.NotificationReceiver) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*entities.NotificationReceiver, error)
|
||||||
|
GetByNotificationAndUser(ctx context.Context, notificationID, userID uuid.UUID) (*entities.NotificationReceiver, error)
|
||||||
|
Update(ctx context.Context, receiver *entities.NotificationReceiver) error
|
||||||
|
ListByUserID(ctx context.Context, userID uuid.UUID, isRead *bool, limit, offset int) ([]*entities.NotificationReceiver, int64, error)
|
||||||
|
CountUnreadByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
|
||||||
|
SoftDeleteByID(ctx context.Context, id uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationReceiverRepositoryImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationReceiverRepository(db *gorm.DB) *NotificationReceiverRepositoryImpl {
|
||||||
|
return &NotificationReceiverRepositoryImpl{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationReceiverRepositoryImpl) Create(ctx context.Context, receiver *entities.NotificationReceiver) error {
|
||||||
|
return r.db.WithContext(ctx).Create(receiver).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationReceiverRepositoryImpl) BulkCreate(ctx context.Context, receivers []*entities.NotificationReceiver) error {
|
||||||
|
if len(receivers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Create(&receivers).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationReceiverRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.NotificationReceiver, error) {
|
||||||
|
var receiver entities.NotificationReceiver
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Notification").
|
||||||
|
First(&receiver, "id = ? AND is_deleted = false", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &receiver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationReceiverRepositoryImpl) GetByNotificationAndUser(ctx context.Context, notificationID, userID uuid.UUID) (*entities.NotificationReceiver, error) {
|
||||||
|
var receiver entities.NotificationReceiver
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("notification_id = ? AND user_id = ? AND is_deleted = false", notificationID, userID).
|
||||||
|
First(&receiver).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &receiver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationReceiverRepositoryImpl) Update(ctx context.Context, receiver *entities.NotificationReceiver) error {
|
||||||
|
return r.db.WithContext(ctx).Save(receiver).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationReceiverRepositoryImpl) ListByUserID(ctx context.Context, userID uuid.UUID, isRead *bool, limit, offset int) ([]*entities.NotificationReceiver, int64, error) {
|
||||||
|
var receivers []*entities.NotificationReceiver
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.NotificationReceiver{}).
|
||||||
|
Where("user_id = ? AND is_deleted = false", userID)
|
||||||
|
|
||||||
|
if isRead != nil {
|
||||||
|
query = query.Where("is_read = ?", *isRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Preload("Notification").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&receivers).Error
|
||||||
|
|
||||||
|
return receivers, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationReceiverRepositoryImpl) CountUnreadByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.NotificationReceiver{}).
|
||||||
|
Where("user_id = ? AND is_read = false AND is_deleted = false", userID).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationReceiverRepositoryImpl) SoftDeleteByID(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Model(&entities.NotificationReceiver{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Updates(map[string]interface{}{"is_deleted": true}).Error
|
||||||
|
}
|
||||||
64
internal/repository/notification_repository.go
Normal file
64
internal/repository/notification_repository.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationRepository interface {
|
||||||
|
Create(ctx context.Context, notification *entities.Notification) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*entities.Notification, error)
|
||||||
|
Update(ctx context.Context, notification *entities.Notification) error
|
||||||
|
Delete(ctx context.Context, id uuid.UUID) error
|
||||||
|
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Notification, int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationRepositoryImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationRepository(db *gorm.DB) *NotificationRepositoryImpl {
|
||||||
|
return &NotificationRepositoryImpl{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationRepositoryImpl) Create(ctx context.Context, notification *entities.Notification) error {
|
||||||
|
return r.db.WithContext(ctx).Create(notification).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Notification, error) {
|
||||||
|
var notification entities.Notification
|
||||||
|
err := r.db.WithContext(ctx).First(¬ification, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ¬ification, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationRepositoryImpl) Update(ctx context.Context, notification *entities.Notification) error {
|
||||||
|
return r.db.WithContext(ctx).Save(notification).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Notification{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Notification, int64, error) {
|
||||||
|
var notifications []*entities.Notification
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).Model(&entities.Notification{})
|
||||||
|
for key, value := range filters {
|
||||||
|
query = query.Where(key+" = ?", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(¬ifications).Error
|
||||||
|
return notifications, total, err
|
||||||
|
}
|
||||||
@ -47,11 +47,12 @@ type Router struct {
|
|||||||
customerPointsHandler *handler.CustomerPointsHandler
|
customerPointsHandler *handler.CustomerPointsHandler
|
||||||
spinGameHandler *handler.SpinGameHandler
|
spinGameHandler *handler.SpinGameHandler
|
||||||
userDeviceHandler *handler.UserDeviceHandler
|
userDeviceHandler *handler.UserDeviceHandler
|
||||||
|
notificationHandler *handler.NotificationHandler
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator) *Router {
|
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -91,6 +92,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
|
|||||||
customerAuthMiddleware: customerAuthMiddleware,
|
customerAuthMiddleware: customerAuthMiddleware,
|
||||||
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
||||||
userDeviceHandler: handler.NewUserDeviceHandler(userDeviceService, userDeviceValidator),
|
userDeviceHandler: handler.NewUserDeviceHandler(userDeviceService, userDeviceValidator),
|
||||||
|
notificationHandler: handler.NewNotificationHandler(notificationService, notificationValidator),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,6 +581,24 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
adminUserDevices.GET("", r.userDeviceHandler.ListDevices)
|
adminUserDevices.GET("", r.userDeviceHandler.ListDevices)
|
||||||
adminUserDevices.GET("/user/:user_id", r.userDeviceHandler.GetDevicesByUser)
|
adminUserDevices.GET("/user/:user_id", r.userDeviceHandler.GetDevicesByUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notification routes - authenticated users manage their own notifications
|
||||||
|
notifications := protected.Group("/notifications")
|
||||||
|
{
|
||||||
|
notifications.GET("", r.notificationHandler.List)
|
||||||
|
notifications.GET("/:id", r.notificationHandler.GetByID)
|
||||||
|
notifications.PUT("/:id/read", r.notificationHandler.MarkAsRead)
|
||||||
|
notifications.PUT("/read-all", r.notificationHandler.MarkAllAsRead)
|
||||||
|
notifications.DELETE("/:id", r.notificationHandler.Delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin notification routes - send and broadcast
|
||||||
|
adminNotifications := protected.Group("/notifications")
|
||||||
|
adminNotifications.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
|
{
|
||||||
|
adminNotifications.POST("/send", r.notificationHandler.Send)
|
||||||
|
adminNotifications.POST("/broadcast", r.notificationHandler.Broadcast)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
154
internal/service/notification_service.go
Normal file
154
internal/service/notification_service.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/processor"
|
||||||
|
"apskel-pos-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationService interface {
|
||||||
|
Send(ctx context.Context, req *contract.SendNotificationRequest, createdBy uuid.UUID) *contract.Response
|
||||||
|
Broadcast(ctx context.Context, req *contract.BroadcastNotificationRequest, organizationID, createdBy uuid.UUID) *contract.Response
|
||||||
|
MarkAsRead(ctx context.Context, receiverID, userID uuid.UUID) *contract.Response
|
||||||
|
MarkAllAsRead(ctx context.Context, userID uuid.UUID) *contract.Response
|
||||||
|
DeleteForUser(ctx context.Context, receiverID, userID uuid.UUID) *contract.Response
|
||||||
|
ListForUser(ctx context.Context, req *contract.ListNotificationsRequest, userID uuid.UUID) *contract.Response
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) *contract.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationServiceImpl struct {
|
||||||
|
notificationProcessor processor.NotificationProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationService(notificationProcessor processor.NotificationProcessor) *NotificationServiceImpl {
|
||||||
|
return &NotificationServiceImpl{
|
||||||
|
notificationProcessor: notificationProcessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) Send(ctx context.Context, req *contract.SendNotificationRequest, createdBy uuid.UUID) *contract.Response {
|
||||||
|
modelReq := &models.SendNotificationRequest{
|
||||||
|
Title: req.Title,
|
||||||
|
Body: req.Body,
|
||||||
|
Type: req.Type,
|
||||||
|
Category: req.Category,
|
||||||
|
Priority: req.Priority,
|
||||||
|
ImageURL: req.ImageURL,
|
||||||
|
ActionURL: req.ActionURL,
|
||||||
|
NotifiableType: req.NotifiableType,
|
||||||
|
NotifiableID: req.NotifiableID,
|
||||||
|
Data: req.Data,
|
||||||
|
ReceiverIDs: req.ReceiverIDs,
|
||||||
|
ScheduledAt: req.ScheduledAt,
|
||||||
|
ExpiredAt: req.ExpiredAt,
|
||||||
|
CreatedBy: &createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.notificationProcessor.Send(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.NotificationServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.NotificationModelResponseToContract(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) Broadcast(ctx context.Context, req *contract.BroadcastNotificationRequest, organizationID, createdBy uuid.UUID) *contract.Response {
|
||||||
|
modelReq := &models.BroadcastNotificationRequest{
|
||||||
|
Title: req.Title,
|
||||||
|
Body: req.Body,
|
||||||
|
Type: req.Type,
|
||||||
|
Category: req.Category,
|
||||||
|
Priority: req.Priority,
|
||||||
|
ImageURL: req.ImageURL,
|
||||||
|
ActionURL: req.ActionURL,
|
||||||
|
NotifiableType: req.NotifiableType,
|
||||||
|
NotifiableID: req.NotifiableID,
|
||||||
|
Data: req.Data,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
ScheduledAt: req.ScheduledAt,
|
||||||
|
ExpiredAt: req.ExpiredAt,
|
||||||
|
CreatedBy: &createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.notificationProcessor.Broadcast(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.NotificationServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.NotificationModelResponseToContract(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) MarkAsRead(ctx context.Context, receiverID, userID uuid.UUID) *contract.Response {
|
||||||
|
resp, err := s.notificationProcessor.MarkAsRead(ctx, receiverID, userID)
|
||||||
|
if err != nil {
|
||||||
|
errResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.NotificationServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.NotificationReceiverModelResponseToContract(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) MarkAllAsRead(ctx context.Context, userID uuid.UUID) *contract.Response {
|
||||||
|
if err := s.notificationProcessor.MarkAllAsRead(ctx, userID); err != nil {
|
||||||
|
errResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.NotificationServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(map[string]interface{}{"message": "All notifications marked as read"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) DeleteForUser(ctx context.Context, receiverID, userID uuid.UUID) *contract.Response {
|
||||||
|
if err := s.notificationProcessor.DeleteForUser(ctx, receiverID, userID); err != nil {
|
||||||
|
errResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.NotificationServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(map[string]interface{}{"message": "Notification deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) ListForUser(ctx context.Context, req *contract.ListNotificationsRequest, userID uuid.UUID) *contract.Response {
|
||||||
|
modelReq := &models.ListNotificationsRequest{
|
||||||
|
Page: req.Page,
|
||||||
|
Limit: req.Limit,
|
||||||
|
UserID: userID,
|
||||||
|
IsRead: req.IsRead,
|
||||||
|
}
|
||||||
|
|
||||||
|
receivers, total, unreadCount, err := s.notificationProcessor.ListForUser(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.NotificationServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := int(math.Ceil(float64(total) / float64(req.Limit)))
|
||||||
|
|
||||||
|
response := contract.ListNotificationsResponse{
|
||||||
|
Notifications: transformer.NotificationReceiverModelResponsesToContracts(receivers),
|
||||||
|
TotalCount: total,
|
||||||
|
UnreadCount: unreadCount,
|
||||||
|
Page: req.Page,
|
||||||
|
Limit: req.Limit,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationServiceImpl) GetByID(ctx context.Context, id uuid.UUID) *contract.Response {
|
||||||
|
resp, err := s.notificationProcessor.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
errResp := contract.NewResponseError(constants.NotFoundErrorCode, constants.NotificationServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.NotificationModelResponseToContract(resp))
|
||||||
|
}
|
||||||
63
internal/transformer/notification_transformer.go
Normal file
63
internal/transformer/notification_transformer.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package transformer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NotificationModelResponseToContract(m *models.NotificationResponse) *contract.NotificationResponse {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &contract.NotificationResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
Title: m.Title,
|
||||||
|
Body: m.Body,
|
||||||
|
Type: m.Type,
|
||||||
|
Category: m.Category,
|
||||||
|
Priority: m.Priority,
|
||||||
|
ImageURL: m.ImageURL,
|
||||||
|
ActionURL: m.ActionURL,
|
||||||
|
NotifiableType: m.NotifiableType,
|
||||||
|
NotifiableID: m.NotifiableID,
|
||||||
|
Data: m.Data,
|
||||||
|
ScheduledAt: m.ScheduledAt,
|
||||||
|
SentAt: m.SentAt,
|
||||||
|
ExpiredAt: m.ExpiredAt,
|
||||||
|
CreatedBy: m.CreatedBy,
|
||||||
|
CreatedAt: m.CreatedAt,
|
||||||
|
UpdatedAt: m.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotificationReceiverModelResponseToContract(m *models.NotificationReceiverResponse) *contract.NotificationReceiverResponse {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp := &contract.NotificationReceiverResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
NotificationID: m.NotificationID,
|
||||||
|
UserID: m.UserID,
|
||||||
|
IsRead: m.IsRead,
|
||||||
|
ReadAt: m.ReadAt,
|
||||||
|
IsDeleted: m.IsDeleted,
|
||||||
|
DeletedAt: m.DeletedAt,
|
||||||
|
CreatedAt: m.CreatedAt,
|
||||||
|
UpdatedAt: m.UpdatedAt,
|
||||||
|
}
|
||||||
|
if m.Notification != nil {
|
||||||
|
resp.Notification = NotificationModelResponseToContract(m.Notification)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotificationReceiverModelResponsesToContracts(ms []*models.NotificationReceiverResponse) []*contract.NotificationReceiverResponse {
|
||||||
|
if ms == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]*contract.NotificationReceiverResponse, len(ms))
|
||||||
|
for i, m := range ms {
|
||||||
|
result[i] = NotificationReceiverModelResponseToContract(m)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
53
internal/validator/notification_validator.go
Normal file
53
internal/validator/notification_validator.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationValidator interface {
|
||||||
|
ValidateSendRequest(req *contract.SendNotificationRequest) (error, string)
|
||||||
|
ValidateBroadcastRequest(req *contract.BroadcastNotificationRequest) (error, string)
|
||||||
|
ValidateListRequest(req *contract.ListNotificationsRequest) (error, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationValidatorImpl struct{}
|
||||||
|
|
||||||
|
func NewNotificationValidator() *NotificationValidatorImpl {
|
||||||
|
return &NotificationValidatorImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NotificationValidatorImpl) ValidateSendRequest(req *contract.SendNotificationRequest) (error, string) {
|
||||||
|
if req.Title == "" {
|
||||||
|
return fmt.Errorf("title is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
if req.Body == "" {
|
||||||
|
return fmt.Errorf("body is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
if len(req.ReceiverIDs) == 0 {
|
||||||
|
return fmt.Errorf("at least one receiver_id is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NotificationValidatorImpl) ValidateBroadcastRequest(req *contract.BroadcastNotificationRequest) (error, string) {
|
||||||
|
if req.Title == "" {
|
||||||
|
return fmt.Errorf("title is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
if req.Body == "" {
|
||||||
|
return fmt.Errorf("body is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NotificationValidatorImpl) ValidateListRequest(req *contract.ListNotificationsRequest) (error, string) {
|
||||||
|
if req.Page < 1 {
|
||||||
|
return fmt.Errorf("page must be greater than 0"), constants.ValidationErrorCode
|
||||||
|
}
|
||||||
|
if req.Limit < 1 || req.Limit > 100 {
|
||||||
|
return fmt.Errorf("limit must be between 1 and 100"), constants.ValidationErrorCode
|
||||||
|
}
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user