type === 2) return true; if (!$activity->quota_enabled) { return $activity->budget === null || bccomp((string)($activity->budget_used ?? '0'), (string)$activity->budget, 2) < 0; } $member = static::memberKey($memberId); if ($member === null) return false; if (static::hasReceived($activity->id, $member)) return true; return $activity->reward_quota === null || (int)$activity->rewarded_count < (int)$activity->reward_quota; } /** Count a member only on their first successful positive reward for this activity. */ public static function award(ActivityReward $activity, $memberId, $amount, callable $grant): bool { $member = static::memberKey($memberId); if ($member === null || !is_numeric($amount) || !preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string)$amount) || bccomp((string)$amount, '0', 2) <= 0) { return false; } return DB::transaction(function () use ($activity, $member, $amount, $grant) { $locked = ActivityReward::where('id', $activity->id)->lockForUpdate()->first(); if (!$locked) return false; if ((int)$locked->type === 2) { $grant(); return true; } if (!$locked->quota_enabled) return ActivityBudgetService::award($locked, $amount, $grant); // Match the locked counter with a current read, even inside an older caller snapshot. $received = static::hasReceived($locked->id, $member, true); if (!$received && $locked->reward_quota !== null && (int)$locked->rewarded_count >= (int)$locked->reward_quota) { return false; } $grant(); if (!$received) { ActivityRewardRecipient::create([ 'activity_id' => $locked->id, 'member_id' => $member, 'created_at' => date('Y-m-d H:i:s'), ]); $locked->rewarded_count = (int)$locked->rewarded_count + 1; $locked->save(); } return true; }); } private static function hasReceived($activityId, string $memberId, bool $currentRead = false): bool { $query = ActivityRewardRecipient::where('activity_id', $activityId)->where('member_id', $memberId); return $currentRead ? $query->lockForUpdate()->first() !== null : $query->exists(); } private static function memberKey($memberId): ?string { if (!is_int($memberId) && !is_string($memberId)) return null; $member = trim((string)$memberId); return $member === '' || $member === '0' || strlen($member) > 64 ? null : $member; } }