ReconcileSweepCommand.php 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. <?php
  2. namespace Webkul\BagistoApi\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use Webkul\BagistoApi\Jobs\ReconcilePendingPaymentJob;
  5. use Webkul\BagistoApi\Models\PaymentAttempt;
  6. use Webkul\Sales\Models\Order;
  7. use Webkul\Sales\Models\OrderProxy;
  8. /**
  9. * Safety net for orders that fell out of the reconciliation queue.
  10. *
  11. * ReconcilePendingPaymentJob normally keeps itself alive by re-dispatching,
  12. * but that chain can break: the initial enqueue failed, no worker consumed
  13. * the queue for a while, or Redis dropped the delayed key. Without a sweep
  14. * such an order would sit in PENDING forever.
  15. *
  16. * Re-dispatching is free of side effects - ShouldBeUniqueUntilProcessing
  17. * drops the duplicate whenever a tick is already queued.
  18. */
  19. class ReconcileSweepCommand extends Command
  20. {
  21. protected $signature = 'bagistoapi:payment:reconcile-sweep
  22. {--dry-run : Report what would be queued without dispatching}';
  23. protected $description = 'Re-queue reconciliation for pending orders whose polling chain was lost';
  24. public function handle(): int
  25. {
  26. if (! config('bagistoapi.reconcile.enabled', true)) {
  27. $this->info('Payment reconciliation is disabled; nothing to sweep.');
  28. return self::SUCCESS;
  29. }
  30. $cutoff = now()->subSeconds((int) config('bagistoapi.reconcile.max_seconds', 10800));
  31. $queue = (string) config('bagistoapi.reconcile.queue', 'payment-reconcile');
  32. $dryRun = (bool) $this->option('dry-run');
  33. /*
  34. * A replayed order can be older than the cutoff and still be inside
  35. * its polling window, because the window is measured from the last
  36. * redirect rather than from order creation.
  37. */
  38. $replayedOrderIds = PaymentAttempt::query()
  39. ->where('status', PaymentAttempt::STATUS_REDIRECTED)
  40. ->where('created_at', '>=', $cutoff)
  41. ->distinct()
  42. ->pluck('order_id')
  43. ->filter()
  44. ->all();
  45. $orderModelClass = OrderProxy::modelClass();
  46. $queued = 0;
  47. $orderModelClass::query()
  48. ->whereIn('status', [Order::STATUS_PENDING, Order::STATUS_PENDING_PAYMENT])
  49. ->where(function ($query) use ($cutoff, $replayedOrderIds) {
  50. $query->where('created_at', '>=', $cutoff);
  51. if ($replayedOrderIds) {
  52. $query->orWhereIn('id', $replayedOrderIds);
  53. }
  54. })
  55. ->orderBy('id')
  56. ->chunkById(200, function ($orders) use (&$queued, $queue, $dryRun) {
  57. foreach ($orders as $order) {
  58. $queued++;
  59. if ($dryRun) {
  60. $this->line("Would queue reconciliation for order {$order->id}");
  61. continue;
  62. }
  63. dispatch((new ReconcilePendingPaymentJob($order->id))->onQueue($queue));
  64. }
  65. });
  66. $this->info($dryRun
  67. ? "Sweep found {$queued} pending order(s)."
  68. : "Sweep queued reconciliation for {$queued} pending order(s).");
  69. return self::SUCCESS;
  70. }
  71. }