売上通知

コマンドシグネチャ

php artisan notification:sale-one-month

目的

notification:sale-one-monthコマンドは、アクティブなサブスクリプションを持つグループの通知設定を処理して、ウィッシュリスト製品およびグループの売上変化を検出します。このコマンドは通知設定をチャンク単位で取得し、設定されたしきい値(比率型または金額型)に基づいて売上データの変化を分析するバックグラウンドジョブをディスパッチし、条件が満たされた場合にグループメンバーに通知を送信します。これにより、ユーザー定義の基準に基づいて製品および製品グループの売上パフォーマンスの変化を自動的に監視することが可能になります。

シーケンス図

ステップ1:コマンド初期化と設定取得

sequenceDiagram
    participant System
    participant SaleCommand as notification:sale-one-month
    participant SettingRepo as NotificationSettingRepository
    participant ConsoleDB[(gb_console.notification_settings)]
    participant Logger
    participant Slack
    
    Note over System,Slack: コマンド初期化(毎日実行)
    
    rect rgb(200, 255, 200)
    Note right of System: 正常ケース - コマンド起動
    System->>SaleCommand: コマンドを実行
    SaleCommand->>Logger: コマンド開始をログ
    
    SaleCommand->>SettingRepo: chunkDataSendNotification(callback, 100)
    SettingRepo->>ConsoleDB: WHERE group has active subscriptionをクエリ
    ConsoleDB-->>SettingRepo: 通知設定を返す
    SettingRepo-->>SaleCommand: 100レコードのチャンクで処理
    SaleCommand->>Logger: 数付きでチャンク処理開始をログ
    end
    
    rect rgb(255, 200, 200)
    Note right of System: エラーハンドリング
    rect rgb(255, 230, 230)
    alt データベース接続エラー
        SettingRepo->>Logger: データベース接続エラーをログ
        SettingRepo->>Slack: データベースエラー通知を送信
    else アクティブサブスクリプションなし
        SettingRepo->>Logger: アクティブサブスクリプションが見つからないことをログ
    end
    end
    end

ステップ2:ジョブディスパッチとレコード処理

sequenceDiagram
    participant SaleCommand as notification:sale-one-month
    participant NotificationJob as SendNotificationJob
    participant QueueSystem as Laravel Queue
    participant Logger
    participant Slack
    
    Note over SaleCommand,Slack: バッチジョブ処理
    
    rect rgb(200, 255, 200)
    Note right of SaleCommand: 正常ケース - ジョブディスパッチ
    loop 各チャンク(100レコード)に対して
        SaleCommand->>QueueSystem: SendNotificationJob(records)をディスパッチ
        QueueSystem->>NotificationJob: 処理のためにジョブをキューに入れる
        SaleCommand->>Logger: チャンクサイズ付きでジョブディスパッチをログ
        
        rect rgb(200, 230, 255)
        Note right of NotificationJob: ジョブ初期化
        NotificationJob->>NotificationJob: リポジトリを初期化
        NotificationJob->>Logger: レコード数付きでジョブ開始をログ
        
        loop チャンク内の各レコードに対して
            NotificationJob->>NotificationJob: 処理タイプを決定(比率/金額)
            NotificationJob->>Logger: レコード処理開始をログ
        end
        end
    end
    end
    
    rect rgb(255, 200, 200)
    Note right of SaleCommand: エラーハンドリング
    rect rgb(255, 230, 230)
    alt キューシステムエラー
        SaleCommand->>Logger: キューディスパッチエラーをログ
        SaleCommand->>Slack: キューエラー通知を送信
    else ジョブ処理エラー
        NotificationJob->>Logger: ジョブ処理エラーをログ
        NotificationJob->>Slack: ジョブエラー通知を送信
    end
    end
    end

ステップ3:比率タイプ処理 - 製品通知

sequenceDiagram
    participant NotificationJob as SendNotificationJob
    participant WishlistRepo as WishlistProductRepository
    participant ProductRepo as ProductRepository
    participant ProductDetailRepo as ProductDetailRepository
    participant ConsoleDB[(gb_console)]
    participant AnalyzerDB[(gb_analyzer)]
    participant Logger
    participant Slack
    
    Note over NotificationJob,Slack: 比率タイプ - 製品処理
    
    rect rgb(200, 255, 200)
    Note right of NotificationJob: 正常ケース - 製品エンティティ処理
    
    NotificationJob->>WishlistRepo: getEntity(notifiable_id)
    WishlistRepo->>ConsoleDB: summaryProductを持つwishlist_productsをクエリ
    ConsoleDB-->>WishlistRepo: ウィッシュリスト製品データを返す
    WishlistRepo-->>NotificationJob: summaryProductを持つウィッシュリスト製品を返す
    
    NotificationJob->>ProductRepo: getByMallProductId(summaryProduct.input)
    ProductRepo->>AnalyzerDB: WHERE mall_product_idのproductsをクエリ
    AnalyzerDB-->>ProductRepo: アナライザー製品を返す
    ProductRepo-->>NotificationJob: アナライザー製品を返す
    
    rect rgb(200, 230, 255)
    Note right of NotificationJob: 日付範囲とデータ取得
    NotificationJob->>NotificationJob: getDateRangeForFrequency(frequency)
    
    NotificationJob->>ProductDetailRepo: getCurrentPeriodRecord(product, currentDateRange)
    ProductDetailRepo->>AnalyzerDB: WHERE metrics_date IN rangeのproduct_detailsをクエリ
    AnalyzerDB-->>ProductDetailRepo: 現在期間データを返す
    ProductDetailRepo-->>NotificationJob: 現在期間の製品詳細を返す
    
    NotificationJob->>ProductDetailRepo: getHistoricalRecord(product, historicalDateRange)
    ProductDetailRepo->>AnalyzerDB: WHERE metrics_date IN historical rangeのproduct_detailsをクエリ
    AnalyzerDB-->>ProductDetailRepo: 過去期間データを返す
    ProductDetailRepo-->>NotificationJob: 過去期間の製品詳細を返す
    end
    
    rect rgb(255, 255, 200)
    Note right of NotificationJob: 計算としきい値チェック
    NotificationJob->>NotificationJob: calculateSalesValue(sales_one_month * price)
    NotificationJob->>NotificationJob: calculate_percentage_change(current, previous)
    NotificationJob->>NotificationJob: shouldNotifyForComparison(comparison_type, change, threshold)
    
    NotificationJob->>Logger: 値付きで計算結果をログ
    end
    end
    
    rect rgb(255, 200, 200)
    Note right of NotificationJob: エラーハンドリング
    rect rgb(255, 230, 230)
    alt 製品が見つからない
        NotificationJob->>Logger: mall_product_id付きで製品が見つからないことをログ
        NotificationJob->>NotificationJob: レコードをスキップして継続
    else 不十分なデータ
        NotificationJob->>Logger: 不十分な売上データをログ
        NotificationJob->>NotificationJob: レコードをスキップして継続
    else 計算エラー
        NotificationJob->>Logger: 入力データ付きで計算エラーをログ
        NotificationJob->>Slack: 計算エラー通知を送信
    end
    end
    end

ステップ4:比率タイプ処理 - グループ通知

sequenceDiagram
    participant NotificationJob as SendNotificationJob
    participant WishlistGroupRepo as WishlistToGroupRepository
    participant ProductRepo as ProductRepository
    participant ProductDetailRepo as ProductDetailRepository
    participant ConsoleDB[(gb_console)]
    participant AnalyzerDB[(gb_analyzer)]
    participant Logger
    participant Slack
    
    Note over NotificationJob,Slack: 比率タイプ - グループ処理
    
    rect rgb(200, 255, 200)
    Note right of NotificationJob: 正常ケース - グループエンティティ処理
    
    NotificationJob->>WishlistGroupRepo: getEntity(notifiable_id)
    WishlistGroupRepo->>ConsoleDB: 製品を持つwishlist_groupsをクエリ
    ConsoleDB-->>WishlistGroupRepo: 製品を持つウィッシュリストグループを返す
    WishlistGroupRepo-->>NotificationJob: 製品を持つウィッシュリストグループを返す
    
    NotificationJob->>NotificationJob: グループ製品からmall_product_idsを抽出
    NotificationJob->>ProductRepo: getByMallProductIds(mallProductIds)
    ProductRepo->>AnalyzerDB: WHERE mall_product_id IN (ids)のproductsをクエリ
    AnalyzerDB-->>ProductRepo: アナライザー製品コレクションを返す
    ProductRepo-->>NotificationJob: アナライザー製品コレクションを返す
    
    rect rgb(200, 230, 255)
    Note right of NotificationJob: 集計データ処理
    NotificationJob->>NotificationJob: getDateRangeForFrequency(frequency)
    
    NotificationJob->>ProductDetailRepo: getLatestProductDetailsInDateRange(productIds, currentRange)
    ProductDetailRepo->>AnalyzerDB: 現在期間のproduct_detailsをクエリ
    AnalyzerDB-->>ProductDetailRepo: 現在期間の詳細を返す
    ProductDetailRepo-->>NotificationJob: 現在期間の詳細を返す
    
    NotificationJob->>ProductDetailRepo: getLatestProductDetailsInDateRange(productIds, historicalRange)
    ProductDetailRepo->>AnalyzerDB: 過去期間のproduct_detailsをクエリ
    AnalyzerDB-->>ProductDetailRepo: 過去期間の詳細を返す
    ProductDetailRepo-->>NotificationJob: 過去期間の詳細を返す
    end
    
    rect rgb(255, 255, 200)
    Note right of NotificationJob: 集計計算
    NotificationJob->>NotificationJob: calculateTotalSalesValue(currentDetails)
    NotificationJob->>NotificationJob: calculateTotalSalesValue(historicalDetails)
    NotificationJob->>NotificationJob: calculate_percentage_change(currentTotal, historicalTotal)
    NotificationJob->>NotificationJob: shouldNotifyForComparison(comparison_type, change, threshold)
    
    NotificationJob->>Logger: 集計計算結果をログ
    end
    end
    
    rect rgb(255, 200, 200)
    Note right of NotificationJob: エラーハンドリング
    rect rgb(255, 230, 230)
    alt グループ内に製品なし
        NotificationJob->>Logger: 空の製品グループをログ
        NotificationJob->>NotificationJob: レコードをスキップして継続
    else 不十分な集計データ
        NotificationJob->>Logger: 不十分な集計データをログ
        NotificationJob->>NotificationJob: レコードをスキップして継続
    end
    end
    end

ステップ5:金額タイプ処理

sequenceDiagram
    participant NotificationJob as SendNotificationJob
    participant WishlistRepo as Repository
    participant ProductRepo as ProductRepository
    participant ProductDetailRepo as ProductDetailRepository
    participant ConsoleDB[(gb_console)]
    participant AnalyzerDB[(gb_analyzer)]
    participant Logger
    participant Slack
    
    Note over NotificationJob,Slack: 金額タイプ処理
    
    rect rgb(200, 255, 200)
    Note right of NotificationJob: 正常ケース - 金額しきい値処理
    
    rect rgb(200, 230, 255)
                alt notifiable_type = WishlistProduct
        Note right of NotificationJob: 個別製品処理
                    NotificationJob->>WishlistRepo: getEntity(notifiable_id)
        WishlistRepo->>ConsoleDB: wishlist_productsをクエリ
        ConsoleDB-->>WishlistRepo: ウィッシュリスト製品を返す
                    WishlistRepo-->>NotificationJob: ウィッシュリスト製品を返す
        
                    NotificationJob->>ProductRepo: getByMallProductId(input)
        ProductRepo->>AnalyzerDB: WHERE mall_product_idのproductsをクエリ
        AnalyzerDB-->>ProductRepo: アナライザー製品を返す
                    ProductRepo-->>NotificationJob: アナライザー製品を返す
        
        NotificationJob->>ProductDetailRepo: getLatestProductDetail()
        ProductDetailRepo->>AnalyzerDB: 最新のproduct_detailsをクエリ
        AnalyzerDB-->>ProductDetailRepo: 最新の製品詳細を返す
        ProductDetailRepo-->>NotificationJob: 最新の製品詳細を返す
        
                    NotificationJob->>NotificationJob: calculateSalesValue(sales_one_month * price)
                else notifiable_type = WishlistGroup
        Note right of NotificationJob: グループ処理
                    NotificationJob->>WishlistRepo: getEntity(notifiable_id)
        WishlistRepo->>ConsoleDB: 製品を持つwishlist_groupsをクエリ
        ConsoleDB-->>WishlistRepo: ウィッシュリストグループを返す
                    WishlistRepo-->>NotificationJob: ウィッシュリストグループを返す
        
        NotificationJob->>ProductRepo: getByMallProductIds(groupProductIds)
        ProductRepo->>AnalyzerDB: WHERE mall_product_id IN (ids)のproductsをクエリ
        AnalyzerDB-->>ProductRepo: アナライザー製品を返す
        ProductRepo-->>NotificationJob: アナライザー製品を返す
        
        NotificationJob->>ProductDetailRepo: getLatestProductDetailsForProducts(productIds)
        ProductDetailRepo->>AnalyzerDB: 製品の最新product_detailsをクエリ
        AnalyzerDB-->>ProductDetailRepo: 最新詳細コレクションを返す
        ProductDetailRepo-->>NotificationJob: 最新詳細コレクションを返す
        
        NotificationJob->>NotificationJob: calculateTotalSalesFromDetails(details)
    end
    end
    
    rect rgb(255, 255, 200)
    Note right of NotificationJob: しきい値評価
    NotificationJob->>NotificationJob: shouldNotifyForThreshold(comparison_type, value, threshold)
    NotificationJob->>Logger: しきい値比較結果をログ
    end
    end
    
    rect rgb(255, 200, 200)
    Note right of NotificationJob: エラーハンドリング
    rect rgb(255, 230, 230)
    alt 売上データ欠落
        NotificationJob->>Logger: 売上データ欠落をログ
        NotificationJob->>NotificationJob: レコードをスキップして継続
    else 無効なしきい値
        NotificationJob->>Logger: 無効なしきい値設定をログ
        NotificationJob->>Slack: 設定エラー通知を送信
    end
    end
    end

ステップ6:通知作成と配信

sequenceDiagram
    participant NotificationJob as SendNotificationJob
    participant GroupRepo as GroupRepository
    participant NotificationSys as SendNotification
    participant NotificationDB[(gb_console.notifications)]
    participant PusherService as Pusher Service
    participant Logger
    participant Slack
    
    Note over NotificationJob,Slack: 通知配信プロセス
    
    rect rgb(200, 255, 200)
    Note right of NotificationJob: 正常ケース - 通知条件が満たされた
    
    NotificationJob->>NotificationJob: prepareNotificationData(entity, record, specificData)
                NotificationJob->>GroupRepo: findById(group_id)
    GroupRepo->>NotificationDB: groupMembers.userを持つgroupsをクエリ
    NotificationDB-->>GroupRepo: メンバー付きグループを返す
    GroupRepo-->>NotificationJob: groupMembers.userを持つグループを返す
    
    NotificationJob->>NotificationJob: グループメンバーからユーザーを抽出
    
    rect rgb(200, 230, 255)
    Note right of NotificationJob: データベース保存
                loop グループ内の各ユーザーに対して
                    NotificationJob->>NotificationSys: new SendNotification(data, product, type, action)
        NotificationSys->>NotificationDB: 通知レコードを保存
        NotificationDB-->>NotificationSys: 通知IDを返す
        NotificationSys-->>NotificationJob: 通知インスタンスを返す
        
        NotificationJob->>NotificationJob: NotificationSentイベントリスナーを登録
        NotificationJob->>NotificationSys: ユーザーに通知を送信
        NotificationSys-->>NotificationJob: NotificationSentイベントをトリガー
            end
        end
    
    rect rgb(230, 200, 255)
    Note right of NotificationJob: リアルタイム配信
    NotificationJob->>NotificationSys: toPusher(userIds, notificationEvents)
    NotificationSys->>PusherService: プライベートチャネルにリアルタイム通知を送信
    PusherService-->>NotificationSys: 配信ステータスを確認
    NotificationSys-->>NotificationJob: 配信ステータスを返す
    
    NotificationJob->>Logger: ユーザー詳細付きで通知成功をログ
        end
    end
    
    rect rgb(255, 200, 200)
    Note right of NotificationJob: エラーハンドリング
    rect rgb(255, 230, 230)
    alt グループが見つからない
        NotificationJob->>Logger: グループが見つからないエラーをログ
        NotificationJob->>NotificationJob: 通知をスキップ
    else グループにユーザーがいない
        NotificationJob->>Logger: グループに有効なユーザーがいないことをログ
        NotificationJob->>NotificationJob: 通知をスキップ
    else データベース保存エラー
        NotificationJob->>Logger: データベース保存エラーをログ
        NotificationJob->>Slack: 保存エラー通知を送信
    else Pusherサービスエラー
        NotificationJob->>Logger: Pusherサービスエラーをログ
        NotificationJob->>NotificationJob: データベースのみの配信にフォールバック
    end
    end
    end

ステップ7:エラーハンドリングとコマンド完了

sequenceDiagram
    participant NotificationJob as SendNotificationJob
    participant SaleCommand as notification:sale-one-month
    participant QueueSystem as Laravel Queue
    participant Logger
    participant Slack
    
    Note over NotificationJob,Slack: エラーハンドリングとコマンド完了
    
    rect rgb(255, 200, 200)
    Note right of NotificationJob: エラーシナリオとリカバリー
    
    rect rgb(255, 230, 230)
    alt データベース接続エラー
        NotificationJob->>Logger: 詳細付きでデータベース接続/クエリエラーをログ
        NotificationJob->>NotificationJob: 指数バックオフリトライを実装
        NotificationJob->>QueueSystem: 遅延付きでジョブを再試行
        NotificationJob->>Slack: データベースエラーアラートを送信
    else 製品データ欠落
        NotificationJob->>Logger: mall_product_id付きで製品データ欠落をログ
        NotificationJob->>NotificationJob: レコードをスキップして処理を継続
    else 計算エラー
        NotificationJob->>Logger: 入力データコンテキスト付きで計算エラーをログ
        NotificationJob->>NotificationJob: レコードをスキップして処理を継続
        NotificationJob->>Slack: 計算エラーアラートを送信
    else Pusherサービスエラー
        NotificationJob->>Logger: APIレスポンス付きでPusherサービスエラーをログ
        NotificationJob->>NotificationJob: データベースのみの配信にフォールバック
        NotificationJob->>Logger: フォールバック配信成功をログ
    else ジョブタイムアウト
        NotificationJob->>Logger: 処理統計付きでジョブタイムアウトをログ
        NotificationJob->>QueueSystem: 再試行のためにジョブをリリース
        NotificationJob->>Slack: タイムアウトアラートを送信
    end
    end
    end
    
    rect rgb(200, 255, 200)
    Note right of SaleCommand: 正常ケース - コマンド完了
    SaleCommand->>Logger: 統計付きでコマンド完了をログ
    SaleCommand->>Logger: 処理された総チャンク数と送信された通知をログ
    SaleCommand->>Logger: 実行時間とパフォーマンスメトリクスをログ
    end

詳細

パラメータ

このコマンドには設定可能なパラメータはありません。すべての設定は以下を通じて処理されます:

  • チャンクサイズ: 100レコードに固定(CHUNK_LIMIT定数として定義)
  • 通知設定: notification_settingsテーブルから取得
  • アクティブサブスクリプション: アクティブサブスクリプションを持つグループのみを処理

頻度

  • スケジュール実行: routes/console.phpで設定された毎日の実行
    • Schedule::command('notification:sale-one-month')->daily();
  • 手動実行: テストや即時処理のために手動でトリガー可能

依存関係

データベース依存関係:

  • 通知設定、グループ、およびユーザーデータ用のコンソールデータベース(gb_console
  • 製品詳細と売上メトリクス用のアナライザーデータベース(gb_analyzer
  • グループのwhereHas('group.activeSubscription')によるアクティブサブスクリプションステータス
  • 適切な関係を持つ有効なウィッシュリスト製品とグループ

外部サービス依存関係:

  • リアルタイム通知配信用のPusherサービス
  • 環境設定での有効なPusher認証情報
  • バックグラウンドジョブ処理用のキューシステム

モデル依存関係:

  • 適切なenum変換(Type、Frequency、ComparisonType)を持つNotificationSettingモデル
  • 通知可能エンティティのためのWishlistProductWishlistGroupモデル
  • アナライザーデータベースからのProductProductDetailモデル
  • 通知配信のためのGroupGroupMemberモデル

出力

テーブル

完全なデータベーススキーマとテーブル関係については、通知概要のデータベーススキーマセクションを参照してください。

主要操作:

  • 読み取り: アクティブなグループサブスクリプションを持つnotification_settings
  • 読み取り: 現在および過去の期間のproduct_detailsからの製品売上データ
  • 作成: 条件を満たす変更に対するnotificationsテーブルの新しいレコード
  • 送信: Pusherサービスを介したリアルタイム通知

サービス

通知処理:

  • SendNotificationJob: 通知チャンクを処理するためのバックグラウンドジョブ
  • SendNotification: データベース保存とPusher配信のためのLaravel通知クラス
  • コンソールとアナライザーデータベース間のデータアクセスのためのリポジトリパターン

計算ロジック:

  • 比率タイプ: 現在と過去の期間の間のパーセンテージ変化を比較
    • 売上値計算にsales_one_month * priceを使用
    • パーセンテージしきい値を持つ増加/減少比較タイプをサポート
  • 金額タイプ: 固定しきい値に対して絶対的な売上値を比較
    • 金額しきい値を持つ超過/下回る比較タイプをサポート

エラーハンドリング

ログ

システムは監視とトラブルシューティングのためのログを生成します:

コマンド実行ログ:

  • 実行コンテキスト付きのコマンド開始と完了
  • チャンク処理統計とタイミング情報
  • 完全な例外コンテキストとスタックトレース付きのエラー詳細

ジョブ処理ログ:

  • 通知設定詳細付きの個別レコード処理結果
  • 特定の製品識別子を持つ製品データ取得の成功/失敗
  • 売上計算結果としきい値比較結果
  • ユーザーとグループ詳細付きの通知配信確認

エラーカテゴリ:

  • 再試行情報付きのデータベース接続失敗
  • 特定のmall_product_id詳細付きの製品データ欠落
  • 入力データコンテキスト付きの計算エラー
  • APIレスポンス詳細付きのPusherサービス失敗

Slack

現在、実際のコードベースには実装されていません。エラー処理はLaravelの組み込みログシステムに依存しています。

トラブルシューティング

データを確認

アクティブな通知設定を確認:

-- アクティブサブスクリプションを持つ通知設定を確認
SELECT ns.id, ns.notifiable_type, ns.notifiable_id, ns.type, 
       ns.comparison_type, ns.comparison_value, ns.frequency,
       g.name as group_name, s.status as subscription_status
FROM notification_settings ns
JOIN groups g ON ns.group_id = g.id
JOIN subscriptions s ON g.id = s.group_id
WHERE s.status = 'active'
ORDER BY ns.created_at DESC;

ウィッシュリスト製品とグループを確認:

-- 通知用のウィッシュリスト製品を検証
SELECT wp.id, wp.summaryProduct.input as mall_product_id,
       wg.name as group_name, wp.created_at
FROM wishlist_products wp
JOIN wishlist_groups wg ON wp.wishlist_group_id = wg.id
WHERE wp.id IN (SELECT notifiable_id FROM notification_settings 
                WHERE notifiable_type = 'App\\Models\\WishlistProduct');

-- 通知用のウィッシュリストグループを検証
SELECT wg.id, wg.name, COUNT(wp.id) as product_count
FROM wishlist_groups wg
LEFT JOIN wishlist_products wp ON wg.id = wp.wishlist_group_id
WHERE wg.id IN (SELECT notifiable_id FROM notification_settings 
                WHERE notifiable_type = 'App\\Models\\WishlistGroup')
GROUP BY wg.id, wg.name;

製品売上データを確認:

-- 売上計算のための最近の製品詳細を確認
SELECT p.id, p.mall_product_id, pd.sales_one_month, pd.price,
       (pd.sales_one_month * pd.price) as sales_value,
       pd.metrics_date, pd.created_at
FROM products p
JOIN product_details pd ON p.id = pd.product_id
WHERE pd.metrics_date >= DATE_SUB(NOW(), INTERVAL 2 MONTH)
ORDER BY pd.metrics_date DESC, p.mall_product_id;

ログを確認

アプリケーションログ:

# 通知コマンド実行を確認
tail -f storage/logs/laravel.log | grep "notification:sale-one-month"

# SendNotificationJobの処理を確認
grep "SendNotificationJob" storage/logs/laravel.log | tail -20

# 通知配信を確認
grep "SendNotification" storage/logs/laravel.log | tail -10

# エラーパターンを確認
grep -E "(ERROR|Exception)" storage/logs/laravel.log | grep -i notification | tail -10

キューステータス:

# 通知ジョブのキューステータスを確認
php artisan queue:work --once --verbose

# 失敗したジョブを確認
php artisan queue:failed

# 失敗した通知ジョブを再試行
php artisan queue:retry all

データベース検証:

-- 作成された最近の通知を確認
SELECT n.id, n.type, n.notifiable_type, n.notifiable_id,
       JSON_EXTRACT(n.data, '$.setting.wishlist_product_name') as product_name,
       JSON_EXTRACT(n.data, '$.compare.change') as change_percentage,
       n.read_at, n.created_at
FROM notifications n
WHERE n.created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)
AND JSON_EXTRACT(n.data, '$.type') = 'sales_one_month_change'
ORDER BY n.created_at DESC;

-- 通知処理統計を確認
SELECT DATE(created_at) as notification_date,
       COUNT(*) as total_notifications,
       COUNT(CASE WHEN read_at IS NOT NULL THEN 1 END) as read_notifications
FROM notifications
WHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(created_at)
ORDER BY notification_date DESC;