Files
lotteryLaravel/app/Services/Ticket/RiskPoolService.php

160 lines
5.3 KiB
PHP

<?php
namespace App\Services\Ticket;
use App\Models\RiskPool;
use App\Lottery\ErrorCode;
use App\Models\TicketItem;
use App\Models\RiskPoolLockLog;
use App\Exceptions\TicketOperationException;
final class RiskPoolService
{
public function __construct(
private readonly PlayCatalogResolver $catalogResolver,
) {}
/**
* @param list<array{number_4d:string, amount:int}> $locks
* @return list<array{number_4d:string, amount:int, warning:bool}>
*/
public function preview(int $drawId, array $locks): array
{
$rows = [];
foreach ($locks as $lock) {
$pool = $this->firstOrMakePool($drawId, $lock['number_4d']);
$remaining = (int) $pool->remaining_amount;
if ($remaining < (int) $lock['amount']) {
throw new TicketOperationException('risk_sold_out', ErrorCode::RiskPoolSoldOut->value);
}
$usage = (int) $pool->total_cap_amount > 0
? ((int) $pool->locked_amount + (int) $lock['amount']) / (int) $pool->total_cap_amount
: 1;
$rows[] = [
'number_4d' => $lock['number_4d'],
'amount' => (int) $lock['amount'],
'warning' => $usage >= 0.8,
];
}
return $rows;
}
/**
* @param list<array{number_4d:string, amount:int}> $locks
*/
public function acquire(int $drawId, ?TicketItem $ticketItem, array $locks): int
{
$total = 0;
foreach ($locks as $lock) {
$pool = RiskPool::query()
->where('draw_id', $drawId)
->where('normalized_number', $lock['number_4d'])
->lockForUpdate()
->first();
if ($pool === null) {
$pool = $this->createPool($drawId, $lock['number_4d']);
$pool = RiskPool::query()
->where('draw_id', $drawId)
->where('normalized_number', $lock['number_4d'])
->lockForUpdate()
->firstOrFail();
}
$amount = (int) $lock['amount'];
if ((int) $pool->remaining_amount < $amount) {
throw new TicketOperationException('risk_sold_out', ErrorCode::RiskPoolSoldOut->value);
}
$pool->forceFill([
'locked_amount' => (int) $pool->locked_amount + $amount,
'remaining_amount' => (int) $pool->remaining_amount - $amount,
'sold_out_status' => ((int) $pool->remaining_amount - $amount) <= 0 ? 1 : 0,
'version' => (int) $pool->version + 1,
])->save();
RiskPoolLockLog::query()->create([
'draw_id' => $drawId,
'normalized_number' => $lock['number_4d'],
'ticket_item_id' => $ticketItem?->id,
'action_type' => 'lock',
'amount' => $amount,
'source_reason' => 'ticket_place',
'created_at' => now(),
]);
$total += $amount;
}
return $total;
}
/**
* @param list<array{number_4d:string, amount:int}> $locks
*/
public function release(int $drawId, ?TicketItem $ticketItem, array $locks): void
{
foreach ($locks as $lock) {
$pool = RiskPool::query()
->where('draw_id', $drawId)
->where('normalized_number', $lock['number_4d'])
->lockForUpdate()
->first();
if ($pool === null) {
continue;
}
$amount = min((int) $lock['amount'], (int) $pool->locked_amount);
$pool->forceFill([
'locked_amount' => (int) $pool->locked_amount - $amount,
'remaining_amount' => (int) $pool->remaining_amount + $amount,
'sold_out_status' => 0,
'version' => (int) $pool->version + 1,
])->save();
RiskPoolLockLog::query()->create([
'draw_id' => $drawId,
'normalized_number' => $lock['number_4d'],
'ticket_item_id' => $ticketItem?->id,
'action_type' => 'release',
'amount' => $amount,
'source_reason' => 'ticket_rollback',
'created_at' => now(),
]);
}
}
private function firstOrMakePool(int $drawId, string $number4d): RiskPool
{
return RiskPool::query()->firstOrCreate(
['draw_id' => $drawId, 'normalized_number' => $number4d],
[
'total_cap_amount' => $this->catalogResolver->resolveCapAmount($drawId, $number4d),
'locked_amount' => 0,
'remaining_amount' => $this->catalogResolver->resolveCapAmount($drawId, $number4d),
'sold_out_status' => 0,
'version' => 0,
],
);
}
private function createPool(int $drawId, string $number4d): RiskPool
{
$cap = $this->catalogResolver->resolveCapAmount($drawId, $number4d);
return RiskPool::query()->create([
'draw_id' => $drawId,
'normalized_number' => $number4d,
'total_cap_amount' => $cap,
'locked_amount' => 0,
'remaining_amount' => $cap,
'sold_out_status' => 0,
'version' => 0,
]);
}
}