0, 'skipped' => 0, 'failed' => 0]; $setting = DB::table('reward_grant_settings')->where('type', $type)->first(); $scheduled = RewardGrantSettingsService::dueAt($setting, Carbon::now()); if (!$scheduled) continue; $model = $type === 'rebate' ? Rebate::class : Backflow::class; $cutoff = $this->cutoff($type, $scheduled); $model::where('status', 0)->where('date', '<', $cutoff) ->chunkById(100, function ($rewards) use ($type, &$result) { foreach ($rewards as $reward) { try { $outcome = DB::transaction(function () use ($type, $reward) { // Configuration changes wait for the current single-row payout only. $setting = DB::table('reward_grant_settings')->where('type', $type)->lockForUpdate()->first(); $scheduled = RewardGrantSettingsService::dueAt($setting, Carbon::now()); if (!$scheduled) return 'stopped'; return RewardGrantService::grant($type, (int) $reward->id, 'system:auto-grant', true, $this->cutoff($type, $scheduled)) ? 'granted' : 'skipped'; }, 3); if ($outcome === 'stopped') return false; ++$result[$type][$outcome]; } catch (Throwable $e) { ++$result[$type]['failed']; Log::error('automatic_reward_grant_failed', ['type' => $type, 'id' => $reward->id, 'exception' => $e]); } } }); } return $result; } private function cutoff(string $type, \Carbon\CarbonInterface $scheduled): string { return $type === 'rebate' ? $scheduled->copy()->setTimezone('America/New_York')->format('Y-m-d') : $scheduled->copy()->setTimezone('Asia/Shanghai')->format('Y-m'); } }