159 lines
6.1 KiB
PHP
159 lines
6.1 KiB
PHP
<?php
|
|
|
|
use App\Models\Draw;
|
|
use App\Models\AuditLog;
|
|
use App\Models\AdminUser;
|
|
use App\Lottery\DrawStatus;
|
|
use App\Models\DrawResultBatch;
|
|
use App\Models\SettlementBatch;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use App\Lottery\DrawResultBatchStatus;
|
|
use App\Support\AdminAuthorizationRegistry;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function createDrawForManualSettlement(
|
|
string $status = 'cooldown',
|
|
bool $withPublishedResult = true,
|
|
): Draw {
|
|
$draw = Draw::query()->create([
|
|
'draw_no' => '20260722-'.random_int(1000, 9999),
|
|
'business_date' => '2026-07-22',
|
|
'sequence_no' => random_int(1000, 9999),
|
|
'status' => $status,
|
|
'start_time' => now()->subMinutes(20),
|
|
'close_time' => now()->subMinutes(10),
|
|
'draw_time' => now()->subMinutes(9),
|
|
'cooling_end_time' => now()->addMinutes(10),
|
|
'current_result_version' => $withPublishedResult ? 1 : 0,
|
|
'settle_version' => 0,
|
|
'is_reopened' => false,
|
|
]);
|
|
|
|
if ($withPublishedResult) {
|
|
DrawResultBatch::query()->create([
|
|
'draw_id' => $draw->id,
|
|
'provider_code' => 'SG',
|
|
'provider_name' => 'Singapore',
|
|
'result_version' => 1,
|
|
'source_type' => 'rng',
|
|
'status' => DrawResultBatchStatus::Published->value,
|
|
'confirmed_at' => now(),
|
|
]);
|
|
}
|
|
|
|
return $draw;
|
|
}
|
|
|
|
function manualSettlementAdminToken(): string
|
|
{
|
|
$admin = AdminUser::query()->create([
|
|
'username' => 'manual_settle_'.bin2hex(random_bytes(3)),
|
|
'name' => 'Manual Settlement',
|
|
'email' => null,
|
|
'password' => Hash::make('secret-strong'),
|
|
'status' => 0,
|
|
]);
|
|
grantSuperAdminRole($admin);
|
|
|
|
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
|
}
|
|
|
|
test('payout manager may skip cooldown and settlement retry is idempotent', function (): void {
|
|
$draw = createDrawForManualSettlement();
|
|
$originalCoolingEnd = $draw->cooling_end_time;
|
|
$token = manualSettlementAdminToken();
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->postJson("/api/v1/admin/draws/{$draw->id}/settlement/run")
|
|
->assertOk()
|
|
->assertJsonPath('data.ran', true)
|
|
->assertJsonPath('data.cooldown_skipped', true)
|
|
->assertJsonPath('data.status', DrawStatus::Settling->value)
|
|
->assertJsonPath('data.settle_version', 1);
|
|
|
|
$draw->refresh();
|
|
expect($draw->status)->toBe(DrawStatus::Settling->value)
|
|
->and($draw->cooling_end_time)->not->toBeNull()
|
|
->and($draw->cooling_end_time->lessThan($originalCoolingEnd))->toBeTrue()
|
|
->and(SettlementBatch::query()->where('draw_id', $draw->id)->count())->toBe(1);
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->postJson("/api/v1/admin/draws/{$draw->id}/settlement/run")
|
|
->assertOk()
|
|
->assertJsonPath('data.ran', true)
|
|
->assertJsonPath('data.cooldown_skipped', false)
|
|
->assertJsonPath('data.settle_version', 1);
|
|
|
|
expect(SettlementBatch::query()->where('draw_id', $draw->id)->count())->toBe(1);
|
|
});
|
|
|
|
test('manual settlement rejects missing results and invalid draw states without mutation', function (): void {
|
|
$token = manualSettlementAdminToken();
|
|
$missingResult = createDrawForManualSettlement(withPublishedResult: false);
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->postJson("/api/v1/admin/draws/{$missingResult->id}/settlement/run")
|
|
->assertStatus(409)
|
|
->assertJsonPath('data.reason', 'draw_result_not_published');
|
|
|
|
expect($missingResult->fresh()->status)->toBe(DrawStatus::Cooldown->value)
|
|
->and(SettlementBatch::query()->where('draw_id', $missingResult->id)->exists())->toBeFalse();
|
|
|
|
$openDraw = createDrawForManualSettlement(status: DrawStatus::Open->value);
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->postJson("/api/v1/admin/draws/{$openDraw->id}/settlement/run")
|
|
->assertStatus(409)
|
|
->assertJsonPath('data.reason', 'draw_not_ready_for_settlement');
|
|
|
|
expect($openDraw->fresh()->status)->toBe(DrawStatus::Open->value)
|
|
->and(SettlementBatch::query()->where('draw_id', $openDraw->id)->exists())->toBeFalse();
|
|
});
|
|
|
|
test('manual settlement resource is restricted to settlement management and audited', function (): void {
|
|
$resource = collect(AdminAuthorizationRegistry::resources())
|
|
->firstWhere('code', 'admin.draws.settlement.run');
|
|
|
|
expect($resource)->not->toBeNull()
|
|
->and($resource['permission_codes'])->toBe(['settlement.batch.manage'])
|
|
->and($resource['is_audit_required'])->toBeTrue();
|
|
|
|
$draw = createDrawForManualSettlement();
|
|
$token = manualSettlementAdminToken();
|
|
$before = AuditLog::query()->count();
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->postJson("/api/v1/admin/draws/{$draw->id}/settlement/run")
|
|
->assertOk();
|
|
|
|
expect(AuditLog::query()->count())->toBe($before + 1);
|
|
});
|
|
|
|
test('draw finance summary exposes provider and result settlement versions', function (): void {
|
|
$draw = createDrawForManualSettlement(status: DrawStatus::Settling->value);
|
|
$result = DrawResultBatch::query()->where('draw_id', $draw->id)->firstOrFail();
|
|
SettlementBatch::query()->create([
|
|
'draw_id' => $draw->id,
|
|
'result_batch_id' => $result->id,
|
|
'settle_version' => 3,
|
|
'status' => 'pending_review',
|
|
'total_ticket_count' => 0,
|
|
'total_win_count' => 0,
|
|
'total_payout_amount' => 0,
|
|
'total_jackpot_payout_amount' => 0,
|
|
'started_at' => now(),
|
|
'finished_at' => now(),
|
|
]);
|
|
$token = manualSettlementAdminToken();
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
->getJson("/api/v1/admin/draws/{$draw->id}/finance-summary")
|
|
->assertOk()
|
|
->assertJsonPath('data.settlement_batches.0.provider_code', 'SG')
|
|
->assertJsonPath('data.settlement_batches.0.provider_name', 'Singapore')
|
|
->assertJsonPath('data.settlement_batches.0.result_version', 1)
|
|
->assertJsonPath('data.settlement_batches.0.settle_version', 3);
|
|
});
|