feat: add notification events handling and MQTT integration

- Implemented notification event publishing with a new NotificationEventPublisher interface.
- Created a noopNotificationEventPublisher for testing purposes.
- Added functionality to publish notification created events via MQTT.
- Introduced a new stream event publisher for handling job logs and updates.
- Added database migration for popup_ads table.
- Created tests for notification events and popup ads functionality.
- Established MQTT connection and publishing helpers for event messages.
This commit is contained in:
2026-03-29 15:47:09 +00:00
parent a910e6c624
commit 863a0ea2f6
42 changed files with 4606 additions and 576 deletions

View File

@@ -318,6 +318,150 @@ func (s *adTemplatesAppService) DeleteAdTemplate(ctx context.Context, req *appv1
return messageResponse("Ad template deleted"), nil
}
func (s *popupAdsAppService) ListPopupAds(ctx context.Context, req *appv1.ListPopupAdsRequest) (*appv1.ListPopupAdsResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
page, limit, offset := adminPageLimitOffset(req.GetPage(), req.GetLimit())
items, total, err := s.popupAdRepository.ListByUser(ctx, result.UserID, limit, offset)
if err != nil {
s.logger.Error("Failed to list popup ads", "error", err)
return nil, status.Error(codes.Internal, "Failed to load popup ads")
}
payload := make([]*appv1.PopupAd, 0, len(items))
for _, item := range items {
copyItem := item
payload = append(payload, toProtoPopupAd(&copyItem))
}
return &appv1.ListPopupAdsResponse{Items: payload, Total: total, Page: page, Limit: limit}, nil
}
func (s *popupAdsAppService) CreatePopupAd(ctx context.Context, req *appv1.CreatePopupAdRequest) (*appv1.CreatePopupAdResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
popupType := strings.ToLower(strings.TrimSpace(req.GetType()))
label := strings.TrimSpace(req.GetLabel())
value := strings.TrimSpace(req.GetValue())
maxTriggers := int32(3)
if req.MaxTriggersPerSession != nil {
maxTriggers = *req.MaxTriggersPerSession
}
if popupType != "url" && popupType != "script" {
return nil, status.Error(codes.InvalidArgument, "Popup ad type must be url or script")
}
if label == "" || value == "" {
return nil, status.Error(codes.InvalidArgument, "Label and value are required")
}
if maxTriggers < 1 {
return nil, status.Error(codes.InvalidArgument, "Max triggers per session must be greater than 0")
}
item := &model.PopupAd{
ID: uuid.New().String(),
UserID: result.UserID,
Type: popupType,
Label: label,
Value: value,
IsActive: model.BoolPtr(req.IsActive == nil || *req.IsActive),
MaxTriggersPerSession: int32Ptr(maxTriggers),
}
if err := s.popupAdRepository.Create(ctx, item); err != nil {
s.logger.Error("Failed to create popup ad", "error", err)
return nil, status.Error(codes.Internal, "Failed to save popup ad")
}
return &appv1.CreatePopupAdResponse{Item: toProtoPopupAd(item)}, nil
}
func (s *popupAdsAppService) UpdatePopupAd(ctx context.Context, req *appv1.UpdatePopupAdRequest) (*appv1.UpdatePopupAdResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Popup ad not found")
}
popupType := strings.ToLower(strings.TrimSpace(req.GetType()))
label := strings.TrimSpace(req.GetLabel())
value := strings.TrimSpace(req.GetValue())
if popupType != "url" && popupType != "script" {
return nil, status.Error(codes.InvalidArgument, "Popup ad type must be url or script")
}
if label == "" || value == "" {
return nil, status.Error(codes.InvalidArgument, "Label and value are required")
}
if req.MaxTriggersPerSession != nil && *req.MaxTriggersPerSession < 1 {
return nil, status.Error(codes.InvalidArgument, "Max triggers per session must be greater than 0")
}
item, err := s.popupAdRepository.GetByIDAndUser(ctx, id, result.UserID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Popup ad not found")
}
s.logger.Error("Failed to load popup ad", "error", err)
return nil, status.Error(codes.Internal, "Failed to save popup ad")
}
item.Type = popupType
item.Label = label
item.Value = value
if req.IsActive != nil {
item.IsActive = model.BoolPtr(*req.IsActive)
}
if req.MaxTriggersPerSession != nil {
item.MaxTriggersPerSession = int32Ptr(*req.MaxTriggersPerSession)
}
if err := s.popupAdRepository.Save(ctx, item); err != nil {
s.logger.Error("Failed to update popup ad", "error", err)
return nil, status.Error(codes.Internal, "Failed to save popup ad")
}
return &appv1.UpdatePopupAdResponse{Item: toProtoPopupAd(item)}, nil
}
func (s *popupAdsAppService) DeletePopupAd(ctx context.Context, req *appv1.DeletePopupAdRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Popup ad not found")
}
rowsAffected, err := s.popupAdRepository.DeleteByIDAndUser(ctx, id, result.UserID)
if err != nil {
s.logger.Error("Failed to delete popup ad", "error", err)
return nil, status.Error(codes.Internal, "Failed to delete popup ad")
}
if rowsAffected == 0 {
return nil, status.Error(codes.NotFound, "Popup ad not found")
}
return messageResponse("Popup ad deleted"), nil
}
func (s *popupAdsAppService) GetActivePopupAd(ctx context.Context, _ *appv1.GetActivePopupAdRequest) (*appv1.GetActivePopupAdResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
return nil, err
}
item, err := s.popupAdRepository.GetActiveByUser(ctx, result.UserID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return &appv1.GetActivePopupAdResponse{}, nil
}
s.logger.Error("Failed to load active popup ad", "error", err)
return nil, status.Error(codes.Internal, "Failed to load popup ad")
}
return &appv1.GetActivePopupAdResponse{Item: toProtoPopupAd(item)}, nil
}
func (s *plansAppService) ListPlans(ctx context.Context, _ *appv1.ListPlansRequest) (*appv1.ListPlansResponse, error) {
if _, err := s.authenticate(ctx); err != nil {
return nil, err