|
@@ -237,7 +237,8 @@ class PromotionService
|
|
|
$this->capacity($activity, $member, $amount);
|
|
$this->capacity($activity, $member, $amount);
|
|
|
$received = DB::table('promotion_rewards')->where('activity_id', $activity->id)->where('user_id', $member->id)->exists();
|
|
$received = DB::table('promotion_rewards')->where('activity_id', $activity->id)->where('user_id', $member->id)->exists();
|
|
|
DB::table('promotion_rewards')->insert(['activity_id' => $activity->id, 'user_id' => $member->id, 'account_id' => $account->id,
|
|
DB::table('promotion_rewards')->insert(['activity_id' => $activity->id, 'user_id' => $member->id, 'account_id' => $account->id,
|
|
|
- 'round_no' => $roundNo, 'amount' => $amount, 'required_flow' => bcmul($amount, $rules['turnover_ratio'], 2),
|
|
|
|
|
|
|
+ 'round_no' => $roundNo, 'amount' => $amount,
|
|
|
|
|
+ 'required_flow' => ($rules['reward_mode'] ?? 'cash') === 'coupon' ? '0.00' : bcmul($amount, $rules['turnover_ratio'], 2),
|
|
|
'status' => 'reserved', 'rule_snapshot' => json_encode($rules), 'created_at' => PromotionFacts::now(), 'updated_at' => PromotionFacts::now()]);
|
|
'status' => 'reserved', 'rule_snapshot' => json_encode($rules), 'created_at' => PromotionFacts::now(), 'updated_at' => PromotionFacts::now()]);
|
|
|
if (!$received) ++$activity->rewarded_count;
|
|
if (!$received) ++$activity->rewarded_count;
|
|
|
$activity->budget_used = bcadd((string)$activity->budget_used, $amount, 2); $activity->save();
|
|
$activity->budget_used = bcadd((string)$activity->budget_used, $amount, 2); $activity->save();
|
|
@@ -296,6 +297,30 @@ class PromotionService
|
|
|
return DB::table('promotion_rewards')->where('id', $reward->id)->first();
|
|
return DB::table('promotion_rewards')->where('id', $reward->id)->first();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** Coupon rewards credit no money; the reserved face value is consumed by the coupons themselves. */
|
|
|
|
|
+ private function issueCoupons(object $reward, User $member): object
|
|
|
|
|
+ {
|
|
|
|
|
+ if ($reward->status !== 'reserved') return $reward;
|
|
|
|
|
+ $rules = $this->json($reward->rule_snapshot); $now = PromotionFacts::now();
|
|
|
|
|
+ $holders = [[(int)$member->id, (string)$member->member_id, 'inviter', $rules['inviter_coupons']]];
|
|
|
|
|
+ foreach (DB::table('users')->whereIn('id', $rules['coupon_invitees'] ?: [0])->orderBy('id')->get(['id', 'member_id']) as $child) {
|
|
|
|
|
+ $holders[] = [(int)$child->id, (string)$child->member_id, 'invitee', $rules['invitee_coupons']];
|
|
|
|
|
+ }
|
|
|
|
|
+ $rows = [];
|
|
|
|
|
+ foreach ($holders as [$userId, $memberId, $role, $tiers]) {
|
|
|
|
|
+ $seq = 0;
|
|
|
|
|
+ foreach ($tiers as $tier) for ($i = 0; $i < $tier['quantity']; $i++) {
|
|
|
|
|
+ $rows[] = ['activity_id' => $reward->activity_id, 'reward_id' => $reward->id, 'user_id' => $userId, 'member_id' => $memberId,
|
|
|
|
|
+ 'role' => $role, 'inviter_user_id' => $member->id, 'seq' => ++$seq, 'min_amount' => $tier['min_amount'], 'discount' => $tier['discount'],
|
|
|
|
|
+ 'categories' => json_encode($rules['coupon_categories']), 'valid_from' => $rules['coupon_valid_from'], 'valid_until' => $rules['coupon_valid_until'],
|
|
|
|
|
+ 'status' => 'unused', 'created_at' => $now, 'updated_at' => $now];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach (array_chunk($rows, 500) as $chunk) DB::table('promotion_coupons')->insertOrIgnore($chunk);
|
|
|
|
|
+ DB::table('promotion_rewards')->where('id', $reward->id)->update(['status' => 'paid', 'updated_at' => $now]);
|
|
|
|
|
+ return DB::table('promotion_rewards')->where('id', $reward->id)->first();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public function claim(int $activityId, int $userId, int $roundNo, ?string $platform = null): array
|
|
public function claim(int $activityId, int $userId, int $roundNo, ?string $platform = null): array
|
|
|
{
|
|
{
|
|
|
return $this->mutate($activityId, $userId, function ($activity, $member, $wallet) use ($roundNo, $platform) {
|
|
return $this->mutate($activityId, $userId, function ($activity, $member, $wallet) use ($roundNo, $platform) {
|
|
@@ -308,13 +333,18 @@ class PromotionService
|
|
|
$snapshot = $this->json($account->application_snapshot); $rules = $snapshot['rules'];
|
|
$snapshot = $this->json($account->application_snapshot); $rules = $snapshot['rules'];
|
|
|
$qualified = $this->lossInvitees($member, $account);
|
|
$qualified = $this->lossInvitees($member, $account);
|
|
|
if (count($qualified) < $rules['invite_count']) $this->invalid('有效邀请人数不足');
|
|
if (count($qualified) < $rules['invite_count']) $this->invalid('有效邀请人数不足');
|
|
|
- $reward ??= $this->reserve($activity, $member, $account, 1, $rules['reward_amount'], $rules + ['qualified_invitees' => $qualified]);
|
|
|
|
|
|
|
+ if (($rules['reward_mode'] ?? 'cash') === 'coupon') {
|
|
|
|
|
+ // Exactly the invitees counted toward the target receive invitee coupons, earliest registrations first.
|
|
|
|
|
+ sort($qualified); $counted = array_slice($qualified, 0, $rules['invite_count']);
|
|
|
|
|
+ $amount = bcadd(Rules::faceValue($rules['inviter_coupons']), bcmul(Rules::faceValue($rules['invitee_coupons']), (string)count($counted), 2), 2);
|
|
|
|
|
+ $reward ??= $this->reserve($activity, $member, $account, 1, $amount, $rules + ['qualified_invitees' => $qualified, 'coupon_invitees' => $counted]);
|
|
|
|
|
+ } else $reward ??= $this->reserve($activity, $member, $account, 1, $rules['reward_amount'], $rules + ['qualified_invitees' => $qualified]);
|
|
|
} elseif ((int)$activity->type === Rules::WHEEL) {
|
|
} elseif ((int)$activity->type === Rules::WHEEL) {
|
|
|
// A reserved, earned reward remains payable after the campaign is unpublished.
|
|
// A reserved, earned reward remains payable after the campaign is unpublished.
|
|
|
$round = $this->round($account);
|
|
$round = $this->round($account);
|
|
|
if (!$reward || !$round || $round->round_no !== $roundNo || $round->status !== 'reached') $this->invalid('本轮尚未达到666');
|
|
if (!$reward || !$round || $round->round_no !== $roundNo || $round->status !== 'reached') $this->invalid('本轮尚未达到666');
|
|
|
} else $this->invalid('首充奖励由充值成功后自动发放');
|
|
} else $this->invalid('首充奖励由充值成功后自动发放');
|
|
|
- $reward = $this->pay($reward, $member, $wallet);
|
|
|
|
|
|
|
+ $reward = ($this->json($reward->rule_snapshot)['reward_mode'] ?? 'cash') === 'coupon' ? $this->issueCoupons($reward, $member) : $this->pay($reward, $member, $wallet);
|
|
|
if ((int)$activity->type === Rules::WHEEL) {
|
|
if ((int)$activity->type === Rules::WHEEL) {
|
|
|
$flags = array_intersect_key($this->json($round->completed_tasks), array_flip(Rules::DAILY));
|
|
$flags = array_intersect_key($this->json($round->completed_tasks), array_flip(Rules::DAILY));
|
|
|
DB::table('promotion_rounds')->where('id', $round->id)->update(['status' => 'claimed', 'claimed_at' => PromotionFacts::now(), 'updated_at' => PromotionFacts::now()]);
|
|
DB::table('promotion_rounds')->where('id', $round->id)->update(['status' => 'claimed', 'claimed_at' => PromotionFacts::now(), 'updated_at' => PromotionFacts::now()]);
|
|
@@ -388,12 +418,24 @@ class PromotionService
|
|
|
}
|
|
}
|
|
|
$result['rewards'] = $account ? DB::table('promotion_rewards')->where('account_id', $account->id)->orderByDesc('round_no')->limit(10)->get()->map(fn ($r) => (array)$r)->all() : [];
|
|
$result['rewards'] = $account ? DB::table('promotion_rewards')->where('account_id', $account->id)->orderByDesc('round_no')->limit(10)->get()->map(fn ($r) => (array)$r)->all() : [];
|
|
|
if ($type === Rules::LOSS) {
|
|
if ($type === Rules::LOSS) {
|
|
|
- if ($account?->applied_at) $result['rules'] = $this->json($account->application_snapshot)['rules'];
|
|
|
|
|
|
|
+ // Applications made before coupon mode existed snapshot cash rules without the coupon keys.
|
|
|
|
|
+ if ($account?->applied_at) $result['rules'] = array_replace(Rules::defaults(Rules::LOSS), $this->json($account->application_snapshot)['rules']);
|
|
|
|
|
+ $result['coupons'] = $this->couponSummary((int)$activity->id, (int)$member->id);
|
|
|
$result['status'] = $result['rewards'] ? 'claimed' : (!$account?->applied_at ? 'not_applied' : ($result['qualified_invitees'] >= $result['rules']['invite_count'] ? 'ready' : 'collecting'));
|
|
$result['status'] = $result['rewards'] ? 'claimed' : (!$account?->applied_at ? 'not_applied' : ($result['qualified_invitees'] >= $result['rules']['invite_count'] ? 'ready' : 'collecting'));
|
|
|
}
|
|
}
|
|
|
return $result;
|
|
return $result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** The member's own coupons from this activity, grouped by tier. */
|
|
|
|
|
+ private function couponSummary(int $activityId, int $userId): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return DB::table('promotion_coupons')->where('activity_id', $activityId)->where('user_id', $userId)
|
|
|
|
|
+ ->selectRaw("role, min_amount, discount, valid_from, valid_until, count(*) as quantity, sum(case when status = 'unused' then 1 else 0 end) as unused")
|
|
|
|
|
+ ->groupBy('role', 'min_amount', 'discount', 'valid_from', 'valid_until')->orderBy('role')->orderBy('min_amount')->get()
|
|
|
|
|
+ ->map(fn ($r) => ['role' => $r->role, 'min_amount' => (string)$r->min_amount, 'discount' => (string)$r->discount, 'valid_from' => $r->valid_from,
|
|
|
|
|
+ 'valid_until' => $r->valid_until, 'quantity' => (int)$r->quantity, 'unused' => (int)$r->unused])->all();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private function wheelToday(PromotionActivity $activity, User $member, ?object $account, ?object $round, array $flags, array $granted): array
|
|
private function wheelToday(PromotionActivity $activity, User $member, ?object $account, ?object $round, array $flags, array $granted): array
|
|
|
{
|
|
{
|
|
|
$rules = $this->rules($activity); $today = substr(PromotionFacts::now(), 0, 10);
|
|
$rules = $this->rules($activity); $today = substr(PromotionFacts::now(), 0, 10);
|