feat: 新增赔率版本和玩法配置版本的删除接口,支持删除草稿版本

This commit is contained in:
2026-05-15 15:30:40 +08:00
parent 5398af0a55
commit c0cd8be0fb
18 changed files with 574 additions and 125 deletions

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Config;
use App\Models\AdminUser;
use App\Models\OddsVersion;
use App\Lottery\ErrorCode;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Lottery\ConfigVersionStatus;
use App\Services\Config\OddsStreamService;
/** DELETE /api/v1/admin/config/odds-versions/{id} */
final class OddsVersionDestroyController extends Controller
{
public function __invoke(Request $request, int $id, OddsStreamService $service): JsonResponse
{
/** @var AdminUser $admin */
$admin = $request->lotteryAdmin();
/** @var OddsVersion $version */
$version = OddsVersion::query()->whereKey($id)->firstOrFail();
if ($version->status === ConfigVersionStatus::Active->value) {
return ApiResponse::error(
'cannot delete active config version',
ErrorCode::ConfigVersionCannotDeleteActive->value,
null,
400,
);
}
$service->deleteVersion($version, $admin, $request);
return ApiResponse::success(['deleted' => true]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Config;
use App\Models\AdminUser;
use App\Lottery\ErrorCode;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use App\Models\PlayConfigVersion;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Lottery\ConfigVersionStatus;
use App\Services\Config\PlayConfigStreamService;
/** DELETE /api/v1/admin/config/play-versions/{id} */
final class PlayConfigVersionDestroyController extends Controller
{
public function __invoke(Request $request, int $id, PlayConfigStreamService $service): JsonResponse
{
/** @var AdminUser $admin */
$admin = $request->lotteryAdmin();
/** @var PlayConfigVersion $version */
$version = PlayConfigVersion::query()->whereKey($id)->firstOrFail();
if ($version->status === ConfigVersionStatus::Active->value) {
return ApiResponse::error(
'cannot delete active config version',
ErrorCode::ConfigVersionCannotDeleteActive->value,
null,
400,
);
}
$service->deleteVersion($version, $admin, $request);
return ApiResponse::success(['deleted' => true]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Config;
use App\Models\AdminUser;
use App\Models\RiskCapVersion;
use App\Lottery\ErrorCode;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Lottery\ConfigVersionStatus;
use App\Services\Config\RiskCapStreamService;
/** DELETE /api/v1/admin/config/risk-cap-versions/{id} */
final class RiskCapVersionDestroyController extends Controller
{
public function __invoke(Request $request, int $id, RiskCapStreamService $service): JsonResponse
{
/** @var AdminUser $admin */
$admin = $request->lotteryAdmin();
/** @var RiskCapVersion $version */
$version = RiskCapVersion::query()->whereKey($id)->firstOrFail();
if ($version->status === ConfigVersionStatus::Active->value) {
return ApiResponse::error(
'cannot delete active config version',
ErrorCode::ConfigVersionCannotDeleteActive->value,
null,
400,
);
}
$service->deleteVersion($version, $admin, $request);
return ApiResponse::success(['deleted' => true]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Ticket;
use Illuminate\Foundation\Http\FormRequest;
abstract class TicketBetRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'draw_id' => ['required', 'string', 'max:32'],
'currency_code' => ['required', 'string', 'max:16'],
'client_trace_id' => ['nullable', 'string', 'max:64'],
'lines' => ['required', 'array', 'min:1', 'max:100'],
'lines.*.number' => ['required', 'string', 'max:32'],
'lines.*.play_code' => ['required', 'string', 'max:32'],
'lines.*.amount' => ['required', 'integer', 'min:1'],
'lines.*.digit_slot' => ['nullable', 'integer', 'min:0', 'max:3'],
'lines.*.dimension' => ['nullable', 'string', 'in:D2,D3,D4'],
];
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Requests\Ticket;
final class TicketPlaceRequest extends TicketPreviewRequest
final class TicketPlaceRequest extends TicketBetRequest
{
/**
* @return array<string, mixed>

View File

@@ -2,30 +2,6 @@
namespace App\Http\Requests\Ticket;
use Illuminate\Foundation\Http\FormRequest;
final class TicketPreviewRequest extends FormRequest
final class TicketPreviewRequest extends TicketBetRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'draw_id' => ['required', 'string', 'max:32'],
'currency_code' => ['required', 'string', 'max:16'],
'client_trace_id' => ['nullable', 'string', 'max:64'],
'lines' => ['required', 'array', 'min:1', 'max:100'],
'lines.*.number' => ['required', 'string', 'max:32'],
'lines.*.play_code' => ['required', 'string', 'max:32'],
'lines.*.amount' => ['required', 'integer', 'min:1'],
'lines.*.digit_slot' => ['nullable', 'integer', 'min:0', 'max:3'],
'lines.*.dimension' => ['nullable', 'string', 'in:D2,D3,D4'],
];
}
}

View File

@@ -89,6 +89,9 @@ enum ErrorCode: int
/** 赔率 / 目录币种未启用或不可下注 */
case ConfigCurrencyInvalid = 2103;
/** 不能删除当前生效active的配置版本 */
case ConfigVersionCannotDeleteActive = 2104;
/* ========== 80008999 玩家 SSO / Bearer 鉴权 ========== */
/** 无 Bearer / 格式错误 / token 为空 */

View File

@@ -149,6 +149,26 @@ final class OddsStreamService
);
}
public function deleteVersion(OddsVersion $version, AdminUser $admin, ?Request $request = null): void
{
$before = $this->snapshotVersion($version);
DB::transaction(function () use ($version): void {
$version->delete();
});
AuditLogger::recordForAdmin(
$admin,
$request,
moduleCode: 'odds',
actionCode: 'delete',
targetType: 'odds_version',
targetId: (string) $version->id,
beforeJson: $before,
afterJson: null,
);
}
/** @return array<string, mixed> */
private function snapshotVersion(OddsVersion $v): array
{

View File

@@ -147,6 +147,26 @@ final class PlayConfigStreamService
);
}
public function deleteVersion(PlayConfigVersion $version, AdminUser $admin, ?Request $request = null): void
{
$before = $this->snapshotVersion($version);
DB::transaction(function () use ($version): void {
$version->delete();
});
AuditLogger::recordForAdmin(
$admin,
$request,
moduleCode: 'play_config',
actionCode: 'delete',
targetType: 'play_config_version',
targetId: (string) $version->id,
beforeJson: $before,
afterJson: null,
);
}
/** @return array<string, mixed> */
private function snapshotVersion(PlayConfigVersion $v): array
{

View File

@@ -131,6 +131,26 @@ final class RiskCapStreamService
);
}
public function deleteVersion(RiskCapVersion $version, AdminUser $admin, ?Request $request = null): void
{
$before = $this->snapshotVersion($version);
DB::transaction(function () use ($version): void {
$version->delete();
});
AuditLogger::recordForAdmin(
$admin,
$request,
moduleCode: 'risk_cap',
actionCode: 'delete',
targetType: 'risk_cap_version',
targetId: (string) $version->id,
beforeJson: $before,
afterJson: null,
);
}
/** @return array<string, mixed> */
private function snapshotVersion(RiskCapVersion $v): array
{

View File

@@ -4,6 +4,7 @@ namespace App\Services\Draw;
use Carbon\Carbon;
use App\Models\Draw;
use App\Models\RiskPool;
use App\Lottery\DrawStatus;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
@@ -144,6 +145,33 @@ final class DrawHallSnapshotBuilder
'seconds_remaining_in_cooldown' => $coolingRemain,
];
$riskAlerts = RiskPool::query()
->where('draw_id', $target->id)
->where(function ($q): void {
$q->where('sold_out_status', 1)
->orWhereRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) >= 0.8');
})
->orderByRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) DESC')
->orderByDesc('locked_amount')
->orderBy('normalized_number')
->limit(500)
->get(['normalized_number', 'total_cap_amount', 'locked_amount', 'remaining_amount', 'sold_out_status'])
->map(fn ($row) => [
'normalized_number' => (string) $row->normalized_number,
'total_cap_amount' => (int) $row->total_cap_amount,
'locked_amount' => (int) $row->locked_amount,
'remaining_amount' => (int) $row->remaining_amount,
'sold_out_status' => (int) $row->sold_out_status,
'is_sold_out' => (int) $row->sold_out_status === 1,
'usage_ratio' => (int) $row->total_cap_amount > 0
? round(((int) $row->locked_amount) / (int) $row->total_cap_amount, 6)
: null,
])
->values()
->all();
$payload['risk_pool_alerts'] = $riskAlerts;
if ($this->showsPublishedResults((string) $target->status)) {
$batchId = DrawResultBatch::query()
->where('draw_id', $target->id)

View File

@@ -41,12 +41,12 @@ final class TicketPlacementService
$expectedVersions = null;
}
$order = DB::transaction(function () use (
$placement = DB::transaction(function () use (
$player,
$currencyCode,
$payload,
$expectedVersions
): TicketOrder {
): array {
$draw = Draw::query()
->where('draw_no', (string) $payload['draw_id'])
->lockForUpdate()
@@ -103,7 +103,7 @@ final class TicketPlacementService
'client_trace_id' => $payload['client_trace_id'] ?? null,
]);
$this->ticketWalletService->deduct($player, $currencyCode, $totalActualDeduct, $order);
$balanceAfter = $this->ticketWalletService->deduct($player, $currencyCode, $totalActualDeduct, $order);
foreach ($evaluatedLines as $evaluated) {
$item = TicketItem::query()->create([
@@ -157,9 +157,15 @@ final class TicketPlacementService
$this->jackpotContribution->recordFromPlacedTicketItem($item, $draw, $currencyCode);
}
return $order;
return [
'order' => $order,
'balance_after' => $balanceAfter,
];
});
$order = $placement['order'];
$balanceAfter = $placement['balance_after'];
$draw = Draw::query()->whereKey($order->draw_id)->firstOrFail();
return [
@@ -174,6 +180,7 @@ final class TicketPlacementService
'total_actual_deduct' => (int) $order->total_actual_deduct,
'total_estimated_payout' => (int) $order->total_estimated_payout,
],
'balance_after' => $balanceAfter,
'items' => TicketItem::query()
->where('order_id', $order->id)
->orderBy('id')

View File

@@ -17,7 +17,7 @@ final class TicketWalletService
private const TXN_DIR_IN = 1;
public function deduct(Player $player, string $currencyCode, int $amountMinor, TicketOrder $order): void
public function deduct(Player $player, string $currencyCode, int $amountMinor, TicketOrder $order): int
{
$wallet = PlayerWallet::query()
->where('player_id', $player->id)
@@ -65,6 +65,8 @@ final class TicketWalletService
'idempotent_key' => $order->client_trace_id,
'remark' => null,
]);
return $after;
}
/**

View File

@@ -137,28 +137,17 @@ final class LotteryTransferService
DB::transaction(function () use ($player, $currencyCode, $amountMinor, $order, $main, $transferNo, $idempotentKey): void {
$wallet = $this->lockLotteryWallet($player, $currencyCode);
$before = (int) $wallet->balance;
$after = $before + $amountMinor;
$wallet->forceFill([
'balance' => $after,
'version' => (int) $wallet->version + 1,
])->save();
WalletTxn::query()->create([
'txn_no' => $this->newTxnNo(),
'player_id' => $player->id,
'wallet_id' => $wallet->id,
'biz_type' => self::BIZ_TRANSFER_IN,
'biz_no' => $transferNo,
'direction' => self::TXN_DIR_IN,
'amount' => $amountMinor,
'balance_before' => $before,
'balance_after' => $after,
'status' => self::TXN_POSTED,
'external_ref_no' => $main->externalRefNo,
'idempotent_key' => $idempotentKey,
'remark' => null,
]);
$this->postLotteryWalletMovement(
wallet: $wallet,
bizType: self::BIZ_TRANSFER_IN,
direction: self::TXN_DIR_IN,
amountMinor: $amountMinor,
bizNo: $transferNo,
externalRefNo: $main->externalRefNo,
idempotentKey: $idempotentKey,
remark: null,
deltaSign: 1,
);
$order->forceFill([
'status' => self::ST_SUCCESS,
@@ -222,34 +211,18 @@ final class LotteryTransferService
try {
DB::transaction(function () use ($player, $currencyCode, $amountMinor, $transferNo, $idempotentKey): void {
$wallet = $this->lockLotteryWallet($player, $currencyCode);
$before = (int) $wallet->balance;
if ($before < $amountMinor) {
throw new WalletOperationException(
'insufficient_balance',
ErrorCode::WalletInsufficientBalance->value,
);
}
$after = $before - $amountMinor;
$wallet->forceFill([
'balance' => $after,
'version' => (int) $wallet->version + 1,
])->save();
WalletTxn::query()->create([
'txn_no' => $this->newTxnNo(),
'player_id' => $player->id,
'wallet_id' => $wallet->id,
'biz_type' => self::BIZ_TRANSFER_OUT,
'biz_no' => $transferNo,
'direction' => self::TXN_DIR_OUT,
'amount' => $amountMinor,
'balance_before' => $before,
'balance_after' => $after,
'status' => self::TXN_POSTED,
'external_ref_no' => null,
'idempotent_key' => $idempotentKey,
'remark' => null,
]);
$this->postLotteryWalletMovement(
wallet: $wallet,
bizType: self::BIZ_TRANSFER_OUT,
direction: self::TXN_DIR_OUT,
amountMinor: $amountMinor,
bizNo: $transferNo,
externalRefNo: null,
idempotentKey: $idempotentKey,
remark: null,
deltaSign: -1,
requireBalance: true,
);
});
} catch (WalletOperationException $e) {
if ($e->lotteryCode === ErrorCode::WalletInsufficientBalance->value) {
@@ -292,28 +265,17 @@ final class LotteryTransferService
DB::transaction(function () use ($player, $currencyCode, $amountMinor, $transferNo, $idempotentKey, $order, $main): void {
$wallet = $this->lockLotteryWallet($player, $currencyCode);
$before = (int) $wallet->balance;
$after = $before + $amountMinor;
$wallet->forceFill([
'balance' => $after,
'version' => (int) $wallet->version + 1,
])->save();
WalletTxn::query()->create([
'txn_no' => $this->newTxnNo(),
'player_id' => $player->id,
'wallet_id' => $wallet->id,
'biz_type' => self::BIZ_TRANSFER_OUT_REFUND,
'biz_no' => $transferNo,
'direction' => self::TXN_DIR_IN,
'amount' => $amountMinor,
'balance_before' => $before,
'balance_after' => $after,
'status' => self::TXN_POSTED,
'external_ref_no' => null,
'idempotent_key' => $idempotentKey,
'remark' => 'withdraw_failed_refund',
]);
$this->postLotteryWalletMovement(
wallet: $wallet,
bizType: self::BIZ_TRANSFER_OUT_REFUND,
direction: self::TXN_DIR_IN,
amountMinor: $amountMinor,
bizNo: $transferNo,
externalRefNo: null,
idempotentKey: $idempotentKey,
remark: 'withdraw_failed_refund',
deltaSign: 1,
);
$order->forceFill([
'status' => self::ST_FAILED,
@@ -386,28 +348,17 @@ final class LotteryTransferService
if ($order->direction === self::DIR_OUT) {
DB::transaction(function () use ($order, $remark): void {
$wallet = $this->lockLotteryWalletById($order->player_id, $order->currency_code);
$before = (int) $wallet->balance;
$after = $before + (int) $order->amount;
$wallet->forceFill([
'balance' => $after,
'version' => (int) $wallet->version + 1,
])->save();
WalletTxn::query()->create([
'txn_no' => $this->newTxnNo(),
'player_id' => (int) $order->player_id,
'wallet_id' => $wallet->id,
'biz_type' => self::BIZ_REVERSAL,
'biz_no' => $order->transfer_no,
'direction' => self::TXN_DIR_IN,
'amount' => (int) $order->amount,
'balance_before' => $before,
'balance_after' => $after,
'status' => self::TXN_POSTED,
'external_ref_no' => null,
'idempotent_key' => null,
'remark' => $remark ?: 'reversal_pending_reconcile',
]);
$this->postLotteryWalletMovement(
wallet: $wallet,
bizType: self::BIZ_REVERSAL,
direction: self::TXN_DIR_IN,
amountMinor: (int) $order->amount,
bizNo: $order->transfer_no,
externalRefNo: null,
idempotentKey: null,
remark: $remark ?: 'reversal_pending_reconcile',
deltaSign: 1,
);
$order->forceFill([
'status' => self::ST_REVERSED,
@@ -701,6 +652,61 @@ final class LotteryTransferService
return 'WX_'.Str::lower(str_replace('-', '', Str::uuid()->toString()));
}
/**
* 统一执行彩票钱包余额变更并记录流水。
*
* @return array{before: int, after: int}
*/
private function postLotteryWalletMovement(
PlayerWallet $wallet,
string $bizType,
int $direction,
int $amountMinor,
string $bizNo,
?string $externalRefNo,
?string $idempotentKey,
?string $remark,
int $deltaSign,
bool $requireBalance = false,
): array {
$before = (int) $wallet->balance;
if ($requireBalance && $deltaSign < 0 && $before < $amountMinor) {
throw new WalletOperationException(
'insufficient_balance',
ErrorCode::WalletInsufficientBalance->value,
);
}
$delta = $amountMinor * $deltaSign;
$after = $before + $delta;
$wallet->forceFill([
'balance' => $after,
'version' => (int) $wallet->version + 1,
])->save();
WalletTxn::query()->create([
'txn_no' => $this->newTxnNo(),
'player_id' => $wallet->player_id,
'wallet_id' => $wallet->id,
'biz_type' => $bizType,
'biz_no' => $bizNo,
'direction' => $direction,
'amount' => $amountMinor,
'balance_before' => $before,
'balance_after' => $after,
'status' => self::TXN_POSTED,
'external_ref_no' => $externalRefNo,
'idempotent_key' => $idempotentKey,
'remark' => $remark,
]);
return [
'before' => $before,
'after' => $after,
];
}
private function failedOrderToException(TransferOrder $order): WalletOperationException
{
if (($order->fail_reason ?? '') === 'insufficient_balance') {

View File

@@ -17,6 +17,8 @@ final class DatabaseSeeder extends Seeder
PlayTypeSeeder::class,
OperationalConfigV1Seeder::class,
OddsPrizeScopesBackfillSeeder::class,
/** 对齐玩法目录与 active/draft 配置行(修复历史子集种子) */
PlayOperationalAlignmentSeeder::class,
LotterySettingsSeeder::class,
]);

View File

@@ -0,0 +1,140 @@
<?php
namespace Database\Seeders;
use App\Models\Currency;
use App\Models\OddsItem;
use App\Models\PlayType;
use App\Models\OddsVersion;
use App\Models\PlayConfigItem;
use Illuminate\Database\Seeder;
use App\Models\PlayConfigVersion;
use Illuminate\Support\Facades\DB;
use App\Support\OddsStandardScopes;
use App\Lottery\ConfigVersionStatus;
/**
* 将「玩法目录」与已存在的玩法配置 / 赔率版本对齐(幂等,可反复执行)。
*
* 典型场景:`play_types` 已扩展,但某条 active/draft `play_config_items` `odds_items`
* 仍是早期子集,后台出现大量「无配置行」或赔率缺档。
*
* 执行php artisan db:seed --class=PlayOperationalAlignmentSeeder
*/
final class PlayOperationalAlignmentSeeder extends Seeder
{
private const MIN_BET = 100;
private const MAX_BET = 500_000_000;
public function run(): void
{
$this->call(PlayTypeSeeder::class);
DB::transaction(function (): void {
$open = [ConfigVersionStatus::Active->value, ConfigVersionStatus::Draft->value];
foreach (PlayConfigVersion::query()->whereIn('status', $open)->orderBy('id')->cursor() as $version) {
$this->syncPlayConfigItems($version);
}
foreach (OddsVersion::query()->whereIn('status', $open)->orderBy('id')->cursor() as $version) {
$this->syncOddsItemsForAllPlayTypes($version);
}
});
}
private function syncPlayConfigItems(PlayConfigVersion $version): void
{
$vid = (int) $version->id;
$existing = PlayConfigItem::query()
->where('version_id', $vid)
->pluck('play_code')
->all();
$have = array_fill_keys($existing, true);
foreach (PlayType::query()->orderBy('sort_order')->orderBy('play_code')->cursor() as $pt) {
if (isset($have[$pt->play_code])) {
continue;
}
PlayConfigItem::query()->create([
'version_id' => $vid,
'play_code' => $pt->play_code,
'is_enabled' => (bool) $pt->is_enabled,
'min_bet_amount' => self::MIN_BET,
'max_bet_amount' => self::MAX_BET,
'display_order' => (int) $pt->sort_order,
'rule_text_zh' => null,
'rule_text_en' => null,
'rule_text_ne' => null,
'extra_config_json' => null,
]);
}
}
private function syncOddsItemsForAllPlayTypes(OddsVersion $version): void
{
$vid = (int) $version->id;
$currencies = OddsItem::query()
->where('version_id', $vid)
->distinct()
->pluck('currency_code')
->filter(static fn ($c) => is_string($c) && $c !== '')
->values();
if ($currencies->isEmpty()) {
$fallback = Currency::query()
->where('is_bettable', true)
->where('is_enabled', true)
->orderBy('code')
->value('code');
if ($fallback === null || $fallback === '') {
return;
}
$currencies = collect([strtoupper((string) $fallback)]);
}
foreach ($currencies as $currencyCode) {
$currencyCode = strtoupper((string) $currencyCode);
foreach (PlayType::query()->orderBy('sort_order')->orderBy('play_code')->cursor() as $pt) {
$playCode = $pt->play_code;
$anchor = OddsItem::query()
->where('version_id', $vid)
->where('play_code', $playCode)
->where('currency_code', $currencyCode)
->orderByDesc('id')
->first();
$rebate = (float) ($anchor?->rebate_rate ?? 0);
$commission = (float) ($anchor?->commission_rate ?? 0);
foreach (OddsStandardScopes::PRESET_ODDS_BY_SCOPE as $scope => $oddsValue) {
$exists = OddsItem::query()
->where('version_id', $vid)
->where('play_code', $playCode)
->where('currency_code', $currencyCode)
->where('prize_scope', $scope)
->exists();
if ($exists) {
continue;
}
OddsItem::query()->create([
'version_id' => $vid,
'play_code' => $playCode,
'prize_scope' => $scope,
'odds_value' => $oddsValue,
'rebate_rate' => $rebate,
'commission_rate' => $commission,
'currency_code' => $currencyCode,
'extra_config_json' => null,
]);
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ use App\Http\Controllers\Api\V1\Admin\Config\OddsItemsReplaceController;
use App\Http\Controllers\Api\V1\Admin\Config\OddsVersionIndexController;
use App\Http\Controllers\Api\V1\Admin\Config\OddsVersionStoreController;
use App\Http\Controllers\Api\V1\Admin\Config\OddsVersionPublishController;
use App\Http\Controllers\Api\V1\Admin\Config\OddsVersionDestroyController;
use App\Http\Controllers\Api\V1\Admin\Config\RiskCapVersionShowController;
use App\Http\Controllers\Api\V1\Admin\Config\RiskCapItemsReplaceController;
use App\Http\Controllers\Api\V1\Admin\Config\RiskCapVersionIndexController;
@@ -18,6 +19,8 @@ use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigItemsReplaceController;
use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionIndexController;
use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionStoreController;
use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionPublishController;
use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionDestroyController;
use App\Http\Controllers\Api\V1\Admin\Config\RiskCapVersionDestroyController;
use App\Http\Controllers\Api\V1\Admin\AdminSettingController;
/**
@@ -84,6 +87,9 @@ Route::middleware('admin.permission:prd.play_switch.manage|prd.odds.manage|prd.r
Route::post('play-versions/{id}/publish', PlayConfigVersionPublishController::class)
->whereNumber('id')
->name('play-versions.publish');
Route::delete('play-versions/{id}', PlayConfigVersionDestroyController::class)
->whereNumber('id')
->name('play-versions.destroy');
// 赔率版本写入
Route::post('odds-versions', OddsVersionStoreController::class)
@@ -94,6 +100,9 @@ Route::middleware('admin.permission:prd.play_switch.manage|prd.odds.manage|prd.r
Route::post('odds-versions/{id}/publish', OddsVersionPublishController::class)
->whereNumber('id')
->name('odds-versions.publish');
Route::delete('odds-versions/{id}', OddsVersionDestroyController::class)
->whereNumber('id')
->name('odds-versions.destroy');
// 封顶版本写入
Route::post('risk-cap-versions', RiskCapVersionStoreController::class)
@@ -104,6 +113,9 @@ Route::middleware('admin.permission:prd.play_switch.manage|prd.odds.manage|prd.r
Route::post('risk-cap-versions/{id}/publish', RiskCapVersionPublishController::class)
->whereNumber('id')
->name('risk-cap-versions.publish');
Route::delete('risk-cap-versions/{id}', RiskCapVersionDestroyController::class)
->whereNumber('id')
->name('risk-cap-versions.destroy');
});
});

View File

@@ -2,8 +2,11 @@
use App\Models\PlayType;
use App\Models\AdminUser;
use App\Models\OddsVersion;
use App\Models\RiskCapVersion;
use App\Models\PlayConfigVersion;
use App\Lottery\ConfigVersionStatus;
use App\Lottery\ErrorCode;
use Database\Seeders\CurrencySeeder;
use Database\Seeders\PlayTypeSeeder;
use Illuminate\Support\Facades\Hash;
@@ -86,3 +89,65 @@ test('admin play config draft publish flow', function (): void {
test('admin play-types requires authentication', function (): void {
$this->getJson('/api/v1/admin/play-types')->assertUnauthorized();
});
test('admin cannot delete active play config version', function (): void {
$token = mintConfigAdminToken();
$active = PlayConfigVersion::query()->where('status', ConfigVersionStatus::Active->value)->firstOrFail();
$this->deleteJson('/api/v1/admin/config/play-versions/'.$active->id, [], [
'Authorization' => 'Bearer '.$token,
])
->assertStatus(400)
->assertJsonPath('code', ErrorCode::ConfigVersionCannotDeleteActive->value);
});
test('admin can delete draft play config version', function (): void {
$token = mintConfigAdminToken();
$create = $this->postJson('/api/v1/admin/config/play-versions', [
'reason' => 'to delete',
], ['Authorization' => 'Bearer '.$token]);
$create->assertOk();
$draftId = (int) $create->json('data.id');
$this->deleteJson('/api/v1/admin/config/play-versions/'.$draftId, [], [
'Authorization' => 'Bearer '.$token,
])
->assertOk()
->assertJsonPath('data.deleted', true);
expect(PlayConfigVersion::query()->whereKey($draftId)->exists())->toBeFalse();
});
test('admin can delete draft odds version', function (): void {
$token = mintConfigAdminToken();
$create = $this->postJson('/api/v1/admin/config/odds-versions', [
'reason' => 'to delete',
], ['Authorization' => 'Bearer '.$token]);
$create->assertOk();
$draftId = (int) $create->json('data.id');
$this->deleteJson('/api/v1/admin/config/odds-versions/'.$draftId, [], [
'Authorization' => 'Bearer '.$token,
])
->assertOk()
->assertJsonPath('data.deleted', true);
expect(OddsVersion::query()->whereKey($draftId)->exists())->toBeFalse();
});
test('admin can delete draft risk cap version', function (): void {
$token = mintConfigAdminToken();
$create = $this->postJson('/api/v1/admin/config/risk-cap-versions', [
'reason' => 'to delete',
], ['Authorization' => 'Bearer '.$token]);
$create->assertOk();
$draftId = (int) $create->json('data.id');
$this->deleteJson('/api/v1/admin/config/risk-cap-versions/'.$draftId, [], [
'Authorization' => 'Bearer '.$token,
])
->assertOk()
->assertJsonPath('data.deleted', true);
expect(RiskCapVersion::query()->whereKey($draftId)->exists())->toBeFalse();
});