feat: 增强代理和玩家管理功能
- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。 - 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。 - 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。 - 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。 - 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
This commit is contained in:
@@ -26,7 +26,7 @@ final class AgentLineStoreController extends Controller
|
||||
$site = $result['site'];
|
||||
$node = $result['agent_node'];
|
||||
|
||||
$payload = AgentLinePresenter::provisioned($site, $node, $result['secrets']);
|
||||
$payload = AgentLinePresenter::provisioned($site, $node);
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\AgentProfile;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -22,10 +23,21 @@ final class AgentNodeChildrenController extends Controller
|
||||
return $denied;
|
||||
}
|
||||
|
||||
$items = $agent_node->children()
|
||||
->orderBy('code')
|
||||
$children = $agent_node->children()->orderBy('code')->get();
|
||||
$profiles = AgentProfile::query()
|
||||
->whereIn('agent_node_id', $children->pluck('id'))
|
||||
->get()
|
||||
->map(static fn (AgentNode $child): array => AgentNodePresenter::item($child))
|
||||
->keyBy('agent_node_id');
|
||||
|
||||
$items = $children
|
||||
->map(static function (AgentNode $child) use ($profiles): array {
|
||||
$profile = $profiles->get($child->id);
|
||||
|
||||
return AgentNodePresenter::item(
|
||||
$child,
|
||||
$profile instanceof AgentProfile ? $profile : null,
|
||||
);
|
||||
})
|
||||
->all();
|
||||
|
||||
return ApiResponse::success(['items' => $items]);
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Middleware\RecordAdminApiAudit;
|
||||
use App\Http\Requests\Admin\AdminAgentProfileUpdateRequest;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\AgentProfile;
|
||||
use App\Services\Agent\AgentNodeService;
|
||||
use App\Services\Agent\AgentProfileService;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Support\AdminAgentScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -22,20 +23,28 @@ final class AgentNodeProfileController extends Controller
|
||||
abort_if($admin === null, 401);
|
||||
abort_if(! AdminAgentScope::nodeVisibleTo($admin, $agent_node), 403);
|
||||
|
||||
$service = app(AgentProfileService::class);
|
||||
$profile = AgentProfile::query()->firstOrNew(['agent_node_id' => $agent_node->id]);
|
||||
$parent = $agent_node->parent_id !== null
|
||||
? AgentNode::query()->find($agent_node->parent_id)
|
||||
: null;
|
||||
|
||||
return ApiResponse::success(app(AgentProfileService::class)->present($profile));
|
||||
return ApiResponse::success([
|
||||
...$service->present($profile),
|
||||
'parent_caps' => $service->parentCapsForNode($parent),
|
||||
'risk_tags' => $agent_node->risk_tags ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(
|
||||
AdminAgentProfileUpdateRequest $request,
|
||||
AgentNode $agent_node,
|
||||
AgentProfileService $service,
|
||||
AgentNodeService $agentNodeService,
|
||||
): JsonResponse {
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
abort_if(! AdminAgentScope::nodeVisibleTo($admin, $agent_node), 403);
|
||||
abort_if(! AdminAgentScope::nodeProfileEditableBy($admin, $agent_node), 403);
|
||||
|
||||
$parent = $agent_node->parent_id !== null
|
||||
? AgentNode::query()->find($agent_node->parent_id)
|
||||
@@ -46,9 +55,33 @@ final class AgentNodeProfileController extends Controller
|
||||
$service->assertChildCapabilityGrantsWithinParent($parent, $payload, $admin);
|
||||
}
|
||||
|
||||
$profile = $service->upsertForNode($agent_node, $payload, $parent);
|
||||
$agentNodeService->syncPrimaryOwnerRoleFromProfile($agent_node, $profile);
|
||||
$beforeProfile = AgentProfile::query()->where('agent_node_id', $agent_node->id)->first();
|
||||
$beforeJson = $beforeProfile !== null ? $service->present($beforeProfile) : [];
|
||||
|
||||
return ApiResponse::success($service->present($profile));
|
||||
if ($request->has('risk_tags')) {
|
||||
$agent_node->risk_tags = array_values(array_unique(array_filter(
|
||||
array_map('strval', $request->input('risk_tags', [])),
|
||||
)));
|
||||
$agent_node->save();
|
||||
}
|
||||
|
||||
$profile = $service->upsertForNode($agent_node, $payload, $parent);
|
||||
$afterJson = array_merge($service->present($profile), [
|
||||
'risk_tags' => $agent_node->risk_tags ?? [],
|
||||
]);
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
$request,
|
||||
moduleCode: 'agent',
|
||||
actionCode: 'agent_profile.update',
|
||||
targetType: 'agent_node',
|
||||
targetId: (string) $agent_node->id,
|
||||
beforeJson: $beforeJson,
|
||||
afterJson: $afterJson,
|
||||
);
|
||||
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||
|
||||
return ApiResponse::success($afterJson);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AgentSettlement\SettlementCenterLedgerService;
|
||||
use App\Services\AgentSettlement\SettlementLedgerListFilters;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Support\PaginationTrait;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 结算中心:信用盘玩家实时流水({@see credit_ledger}),与关账后 {@see settlement_bills} 不同。
|
||||
*/
|
||||
final class AdminCreditLedgerIndexController extends Controller
|
||||
{
|
||||
use PaginationTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly SettlementCenterLedgerService $ledgerService,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$adminSiteId = (int) $request->query('admin_site_id', 0);
|
||||
abort_if($adminSiteId <= 0, 422, 'admin_site_id required');
|
||||
abort_if(! AdminAgentSettlementScope::siteAccessible($admin, $adminSiteId), 403);
|
||||
|
||||
$siteCode = (string) DB::table('admin_sites')->where('id', $adminSiteId)->value('code');
|
||||
abort_if($siteCode === '', 422, 'admin_site not found');
|
||||
|
||||
$periodId = (int) $request->query('settlement_period_id', 0);
|
||||
if ($periodId > 0) {
|
||||
abort_if(! AdminAgentSettlementScope::periodAccessible($admin, $periodId), 403);
|
||||
}
|
||||
|
||||
$filters = SettlementLedgerListFilters::fromQuery(array_merge(
|
||||
$request->query(),
|
||||
$periodId > 0 ? ['settlement_period_id' => $periodId] : [],
|
||||
));
|
||||
|
||||
$perPage = $this->perPage($request, 'per_page', 20, 100);
|
||||
$page = $this->page($request);
|
||||
|
||||
$result = $this->ledgerService->listUnified(
|
||||
$admin,
|
||||
$siteCode,
|
||||
$page,
|
||||
$perPage,
|
||||
$filters,
|
||||
);
|
||||
|
||||
return ApiResponse::success($result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentSettlementAdjustmentIndexController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$periodId = (int) $request->query('settlement_period_id', 0);
|
||||
$adminSiteId = (int) $request->query('admin_site_id', 0);
|
||||
$adjustmentType = trim((string) $request->query('adjustment_type', ''));
|
||||
|
||||
$query = DB::table('settlement_adjustments as sa')
|
||||
->leftJoin('settlement_periods as sp', 'sp.id', '=', 'sa.settlement_period_id')
|
||||
->leftJoin('settlement_bills as sb', 'sb.id', '=', 'sa.original_bill_id')
|
||||
->select([
|
||||
'sa.*',
|
||||
'sp.period_start',
|
||||
'sp.period_end',
|
||||
'sp.admin_site_id',
|
||||
'sb.bill_type as original_bill_type',
|
||||
'sb.owner_type as original_owner_type',
|
||||
'sb.owner_id as original_owner_id',
|
||||
])
|
||||
->orderByDesc('sa.id');
|
||||
|
||||
if ($periodId > 0) {
|
||||
$query->where('sa.settlement_period_id', $periodId);
|
||||
}
|
||||
|
||||
if ($adminSiteId > 0) {
|
||||
$query->where('sp.admin_site_id', $adminSiteId);
|
||||
}
|
||||
|
||||
if ($adjustmentType !== '') {
|
||||
$query->where('sa.adjustment_type', $adjustmentType);
|
||||
}
|
||||
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds !== null) {
|
||||
if ($siteIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
} else {
|
||||
$query->whereIn('sp.admin_site_id', $siteIds);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse::success([
|
||||
'items' => $query->limit(200)->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Middleware\RecordAdminApiAudit;
|
||||
use App\Http\Requests\Admin\AdminSettlementBillAdjustmentRequest;
|
||||
use App\Services\AgentSettlement\AgentSettlementBillAdjustmentService;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentSettlementBillAdjustmentController extends Controller
|
||||
{
|
||||
public function __invoke(
|
||||
AdminSettlementBillAdjustmentRequest $request,
|
||||
int $settlement_bill,
|
||||
AgentSettlementBillAdjustmentService $adjustments,
|
||||
): JsonResponse {
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
abort_if(! AdminAgentSettlementScope::billAccessible($admin, $settlement_bill), 404);
|
||||
|
||||
$before = DB::table('settlement_bills')->where('id', $settlement_bill)->first();
|
||||
abort_if($before === null, 404);
|
||||
|
||||
$newBillId = $adjustments->createAdjustment(
|
||||
$settlement_bill,
|
||||
(int) $request->validated('amount'),
|
||||
(string) ($request->validated('adjustment_type') ?? 'adjustment'),
|
||||
$request->validated('reason'),
|
||||
(int) $admin->id,
|
||||
);
|
||||
|
||||
$after = DB::table('settlement_bills')->where('id', $newBillId)->first();
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
$request,
|
||||
moduleCode: 'settlement',
|
||||
actionCode: 'settlement_bill.adjustment',
|
||||
targetType: 'settlement_bill',
|
||||
targetId: (string) $newBillId,
|
||||
beforeJson: (array) $before,
|
||||
afterJson: (array) $after,
|
||||
);
|
||||
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||
|
||||
return ApiResponse::success([
|
||||
'original_bill_id' => $settlement_bill,
|
||||
'adjustment_bill_id' => $newBillId,
|
||||
'bill' => $after,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Middleware\RecordAdminApiAudit;
|
||||
use App\Http\Requests\Admin\AdminSettlementBillBadDebtRequest;
|
||||
use App\Services\AgentSettlement\AgentSettlementBadDebtService;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentSettlementBillBadDebtWriteOffController extends Controller
|
||||
{
|
||||
public function __invoke(
|
||||
AdminSettlementBillBadDebtRequest $request,
|
||||
int $settlement_bill,
|
||||
AgentSettlementBadDebtService $badDebt,
|
||||
): JsonResponse {
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
abort_if(! AdminAgentSettlementScope::billAccessible($admin, $settlement_bill), 404);
|
||||
|
||||
$before = DB::table('settlement_bills')->where('id', $settlement_bill)->first();
|
||||
abort_if($before === null, 404);
|
||||
|
||||
$archiveBillId = $badDebt->writeOff(
|
||||
$settlement_bill,
|
||||
$request->validated('reason'),
|
||||
(int) $admin->id,
|
||||
);
|
||||
|
||||
$after = DB::table('settlement_bills')->where('id', $settlement_bill)->first();
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
$request,
|
||||
moduleCode: 'settlement',
|
||||
actionCode: 'settlement_bill.bad_debt',
|
||||
targetType: 'settlement_bill',
|
||||
targetId: (string) $settlement_bill,
|
||||
beforeJson: (array) $before,
|
||||
afterJson: (array) $after,
|
||||
);
|
||||
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||
|
||||
return ApiResponse::success([
|
||||
'original_bill_id' => $settlement_bill,
|
||||
'bad_debt_bill_id' => $archiveBillId,
|
||||
'bill' => $after,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,8 @@ namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Middleware\RecordAdminApiAudit;
|
||||
use App\Models\Player;
|
||||
use App\Services\AgentSettlement\SettlementPaymentService;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Services\Player\PlayerCreditService;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -18,7 +17,7 @@ final class AgentSettlementBillConfirmController extends Controller
|
||||
public function __invoke(
|
||||
Request $request,
|
||||
int $settlement_bill,
|
||||
PlayerCreditService $creditService,
|
||||
SettlementPaymentService $payments,
|
||||
): JsonResponse {
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
@@ -28,21 +27,7 @@ final class AgentSettlementBillConfirmController extends Controller
|
||||
$bill = DB::table('settlement_bills')->where('id', $settlement_bill)->first();
|
||||
abort_if($bill === null, 404);
|
||||
|
||||
$unpaid = (int) $bill->unpaid_amount;
|
||||
DB::table('settlement_bills')->where('id', $settlement_bill)->update([
|
||||
'paid_amount' => (int) $bill->paid_amount + $unpaid,
|
||||
'unpaid_amount' => 0,
|
||||
'status' => 'confirmed',
|
||||
'confirmed_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if ($bill->owner_type === 'player' && (int) $bill->owner_id > 0) {
|
||||
$player = Player::query()->find((int) $bill->owner_id);
|
||||
if ($player !== null) {
|
||||
$creditService->releaseFromSettlement($player, $unpaid, $settlement_bill);
|
||||
}
|
||||
}
|
||||
$payments->confirmBill($settlement_bill);
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
@@ -51,8 +36,8 @@ final class AgentSettlementBillConfirmController extends Controller
|
||||
actionCode: 'settlement_bill.confirm',
|
||||
targetType: 'settlement_bill',
|
||||
targetId: (string) $settlement_bill,
|
||||
beforeJson: ['status' => (string) $bill->status, 'unpaid_amount' => $unpaid],
|
||||
afterJson: ['status' => 'confirmed', 'paid_amount' => (int) $bill->paid_amount + $unpaid],
|
||||
beforeJson: ['status' => (string) $bill->status],
|
||||
afterJson: ['status' => 'confirmed'],
|
||||
);
|
||||
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentSettlementBillIndexController extends Controller
|
||||
@@ -17,15 +18,142 @@ final class AgentSettlementBillIndexController extends Controller
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$periodId = (int) $request->query('settlement_period_id', 0);
|
||||
$query = DB::table('settlement_bills')->orderByDesc('id');
|
||||
$adminSiteId = (int) $request->query('admin_site_id', 0);
|
||||
|
||||
$query = DB::table('settlement_bills as sb')
|
||||
->leftJoin('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id')
|
||||
->select([
|
||||
'sb.*',
|
||||
'sp.period_start',
|
||||
'sp.period_end',
|
||||
'sp.admin_site_id',
|
||||
])
|
||||
->orderByDesc('sb.id');
|
||||
|
||||
if ($periodId > 0) {
|
||||
$query->where('settlement_period_id', $periodId);
|
||||
$query->where('sb.settlement_period_id', $periodId);
|
||||
}
|
||||
|
||||
AdminAgentSettlementScope::applyToBillsQuery($query, $admin);
|
||||
if ($adminSiteId > 0) {
|
||||
$query->where('sp.admin_site_id', $adminSiteId);
|
||||
}
|
||||
|
||||
$billType = (string) $request->query('bill_type', '');
|
||||
if ($billType !== '') {
|
||||
$query->where('sb.bill_type', $billType);
|
||||
}
|
||||
|
||||
$scope = (string) $request->query('scope', '');
|
||||
match ($scope) {
|
||||
'pending_confirm' => $query->where('sb.status', 'pending_confirm'),
|
||||
'awaiting_payment' => $query
|
||||
->whereIn('sb.status', ['confirmed', 'partial_paid', 'overdue'])
|
||||
->where('sb.unpaid_amount', '>', 0),
|
||||
'settled' => $query->where('sb.status', 'settled'),
|
||||
'adjustment' => $query->whereIn('sb.bill_type', ['adjustment', 'reversal']),
|
||||
default => null,
|
||||
};
|
||||
|
||||
AdminAgentSettlementScope::applyToBillsQuery($query, $admin, 'sb');
|
||||
|
||||
/** @var Collection<int, object> $items */
|
||||
$items = $query->limit(200)->get();
|
||||
|
||||
return ApiResponse::success([
|
||||
'items' => $query->limit(100)->get(),
|
||||
'items' => $this->enrichBillRows($items),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, object> $items
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function enrichBillRows(Collection $items): array
|
||||
{
|
||||
if ($items->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$playerIds = [];
|
||||
$agentIds = [];
|
||||
foreach ($items as $row) {
|
||||
if ((string) $row->owner_type === 'player') {
|
||||
$playerIds[] = (int) $row->owner_id;
|
||||
} elseif ((string) $row->owner_type === 'agent') {
|
||||
$agentIds[] = (int) $row->owner_id;
|
||||
}
|
||||
if ((string) $row->counterparty_type === 'agent' && (int) $row->counterparty_id > 0) {
|
||||
$agentIds[] = (int) $row->counterparty_id;
|
||||
}
|
||||
}
|
||||
|
||||
$players = $playerIds !== []
|
||||
? DB::table('players')
|
||||
->whereIn('id', array_unique($playerIds))
|
||||
->select(['id', 'username', 'site_player_id', 'funding_mode', 'auth_source'])
|
||||
->get()
|
||||
->keyBy('id')
|
||||
: collect();
|
||||
$agents = $agentIds !== []
|
||||
? DB::table('agent_nodes')->whereIn('id', array_unique($agentIds))->get()->keyBy('id')
|
||||
: collect();
|
||||
|
||||
$out = [];
|
||||
foreach ($items as $row) {
|
||||
$item = (array) $row;
|
||||
$item['owner_label'] = $this->resolvePartyLabel(
|
||||
(string) $row->owner_type,
|
||||
(int) $row->owner_id,
|
||||
$players,
|
||||
$agents,
|
||||
);
|
||||
$item['counterparty_label'] = $this->resolvePartyLabel(
|
||||
(string) $row->counterparty_type,
|
||||
(int) $row->counterparty_id,
|
||||
$players,
|
||||
$agents,
|
||||
);
|
||||
if ((string) $row->owner_type === 'player') {
|
||||
$player = $players->get((int) $row->owner_id);
|
||||
$item['owner_funding_mode'] = $player !== null ? (string) ($player->funding_mode ?? '') : null;
|
||||
$item['owner_auth_source'] = $player !== null ? $player->auth_source : null;
|
||||
}
|
||||
$out[] = $item;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, object> $players
|
||||
* @param Collection<int, object> $agents
|
||||
*/
|
||||
private function resolvePartyLabel(
|
||||
string $type,
|
||||
int $id,
|
||||
Collection $players,
|
||||
Collection $agents,
|
||||
): string {
|
||||
if ($type === 'platform' || $id <= 0) {
|
||||
return 'platform';
|
||||
}
|
||||
|
||||
if ($type === 'player') {
|
||||
$player = $players->get($id);
|
||||
|
||||
return $player !== null
|
||||
? (string) ($player->username ?: $player->site_player_id)
|
||||
: "player#{$id}";
|
||||
}
|
||||
|
||||
if ($type === 'agent') {
|
||||
$agent = $agents->get($id);
|
||||
|
||||
return $agent !== null
|
||||
? (string) ($agent->name ?: $agent->code)
|
||||
: "agent#{$id}";
|
||||
}
|
||||
|
||||
return "{$type}#{$id}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Middleware\RecordAdminApiAudit;
|
||||
use App\Http\Requests\Admin\AdminSettlementBillPaymentRequest;
|
||||
use App\Services\AgentSettlement\SettlementPaymentService;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentSettlementBillPaymentController extends Controller
|
||||
{
|
||||
public function __invoke(
|
||||
AdminSettlementBillPaymentRequest $request,
|
||||
int $settlement_bill,
|
||||
SettlementPaymentService $payments,
|
||||
): JsonResponse {
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
abort_if(! AdminAgentSettlementScope::billAccessible($admin, $settlement_bill), 404);
|
||||
|
||||
$before = DB::table('settlement_bills')->where('id', $settlement_bill)->first();
|
||||
abort_if($before === null, 404);
|
||||
|
||||
$validated = $request->validated();
|
||||
$payments->recordPayment(
|
||||
$settlement_bill,
|
||||
(int) $validated['amount'],
|
||||
(int) $admin->id,
|
||||
[
|
||||
'method' => $validated['method'] ?? null,
|
||||
'proof' => $validated['proof'] ?? null,
|
||||
'remark' => $validated['remark'] ?? null,
|
||||
],
|
||||
);
|
||||
|
||||
$after = DB::table('settlement_bills')->where('id', $settlement_bill)->first();
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
$request,
|
||||
moduleCode: 'settlement',
|
||||
actionCode: 'settlement_bill.payment',
|
||||
targetType: 'settlement_bill',
|
||||
targetId: (string) $settlement_bill,
|
||||
beforeJson: (array) $before,
|
||||
afterJson: (array) $after,
|
||||
);
|
||||
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||
|
||||
return ApiResponse::success(['bill' => $after]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentSettlementBillShowController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, int $settlement_bill): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
abort_if(! AdminAgentSettlementScope::billAccessible($admin, $settlement_bill), 404);
|
||||
|
||||
$bill = DB::table('settlement_bills')->where('id', $settlement_bill)->first();
|
||||
abort_if($bill === null, 404);
|
||||
|
||||
$payments = DB::table('payment_records')
|
||||
->where('settlement_bill_id', $settlement_bill)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$rebateAllocations = DB::table('rebate_allocations')
|
||||
->where('settlement_bill_id', $settlement_bill)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$adjustments = DB::table('settlement_adjustments')
|
||||
->where('original_bill_id', $settlement_bill)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
$meta = $bill->meta_json ?? null;
|
||||
$tierSettlements = null;
|
||||
if (is_string($meta) && $meta !== '') {
|
||||
$decoded = json_decode($meta, true);
|
||||
$tierSettlements = is_array($decoded) ? ($decoded['edge'] ?? $decoded['tier_settlements'] ?? null) : null;
|
||||
}
|
||||
|
||||
return ApiResponse::success([
|
||||
'bill' => $bill,
|
||||
'payments' => $payments,
|
||||
'rebate_allocations' => $rebateAllocations,
|
||||
'adjustments' => $adjustments,
|
||||
'tier_edge' => $tierSettlements,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentSettlementPaymentIndexController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$periodId = (int) $request->query('settlement_period_id', 0);
|
||||
$adminSiteId = (int) $request->query('admin_site_id', 0);
|
||||
|
||||
$query = DB::table('payment_records as pr')
|
||||
->join('settlement_bills as sb', 'sb.id', '=', 'pr.settlement_bill_id')
|
||||
->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id')
|
||||
->select([
|
||||
'pr.*',
|
||||
'sb.bill_type',
|
||||
'sb.owner_type',
|
||||
'sb.owner_id',
|
||||
'sb.counterparty_type',
|
||||
'sb.counterparty_id',
|
||||
'sp.period_start',
|
||||
'sp.period_end',
|
||||
'sp.admin_site_id',
|
||||
])
|
||||
->orderByDesc('pr.id');
|
||||
|
||||
if ($periodId > 0) {
|
||||
$query->where('sb.settlement_period_id', $periodId);
|
||||
}
|
||||
|
||||
if ($adminSiteId > 0) {
|
||||
$query->where('sp.admin_site_id', $adminSiteId);
|
||||
}
|
||||
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds !== null) {
|
||||
if ($siteIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
} else {
|
||||
$query->whereIn('sp.admin_site_id', $siteIds);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse::success([
|
||||
'items' => $query->limit(200)->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AgentSettlement\AgentSettlementPeriodSummaryService;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentSettlementPeriodIndexController extends Controller
|
||||
{
|
||||
public function __invoke(
|
||||
Request $request,
|
||||
AgentSettlementPeriodSummaryService $summaryService,
|
||||
): JsonResponse {
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$query = DB::table('settlement_periods')->orderByDesc('id');
|
||||
AdminAgentSettlementScope::applyToPeriodsQuery($query, $admin);
|
||||
|
||||
$siteId = (int) $request->query('admin_site_id', 0);
|
||||
if ($siteId > 0) {
|
||||
$query->where('admin_site_id', $siteId);
|
||||
}
|
||||
|
||||
$periods = $query->limit(100)->get();
|
||||
|
||||
return ApiResponse::success([
|
||||
'items' => $summaryService->attachToPeriodRows($periods),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AgentSettlement\AgentSettlementReportQueryService;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** @deprecated 请用 AgentSettlementReportShowController?type=summary */
|
||||
final class AgentSettlementReportIndexController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, AgentSettlementReportQueryService $reports): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$periodId = (int) $request->query('settlement_period_id', 0);
|
||||
|
||||
return ApiResponse::success($reports->summary($admin, $periodId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AgentSettlement\AgentSettlementReportQueryService;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
final class AgentSettlementReportShowController extends Controller
|
||||
{
|
||||
private const TYPES = [
|
||||
'summary',
|
||||
'player_win_loss',
|
||||
'agent_share',
|
||||
'rebate',
|
||||
'credit',
|
||||
'unpaid_bills',
|
||||
'overdue',
|
||||
'platform_pnl',
|
||||
'draw_period',
|
||||
];
|
||||
|
||||
public function __invoke(Request $request, AgentSettlementReportQueryService $reports): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$type = (string) $request->query('type', 'summary');
|
||||
abort_unless(in_array($type, self::TYPES, true), 404);
|
||||
|
||||
$periodId = (int) $request->query('settlement_period_id', 0);
|
||||
$period = $this->resolvePeriod($periodId, $request);
|
||||
|
||||
$data = match ($type) {
|
||||
'summary' => $reports->summary($admin, $periodId),
|
||||
'player_win_loss' => [
|
||||
'items' => $reports->playerWinLoss($admin, $periodId, $period['start'], $period['end']),
|
||||
],
|
||||
'agent_share' => [
|
||||
'items' => $reports->agentShare($admin, $period['start'], $period['end']),
|
||||
],
|
||||
'rebate' => $reports->rebate($admin, $periodId, $period['start'], $period['end']),
|
||||
'credit' => $reports->credit($admin),
|
||||
'unpaid_bills' => [
|
||||
'items' => $reports->unpaidBills($admin, $periodId),
|
||||
],
|
||||
'overdue' => [
|
||||
'items' => $reports->overdue($admin),
|
||||
],
|
||||
'platform_pnl' => $periodId > 0
|
||||
? $reports->platformPnl($admin, $periodId)
|
||||
: ['error' => 'settlement_period_id_required'],
|
||||
'draw_period' => [
|
||||
'items' => $reports->drawPeriod($admin, $period['start'], $period['end']),
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
|
||||
return ApiResponse::success([
|
||||
'type' => $type,
|
||||
'settlement_period_id' => $periodId > 0 ? $periodId : null,
|
||||
'period_start' => $period['start'],
|
||||
'period_end' => $period['end'],
|
||||
'data' => $data,
|
||||
'footnote' => $type === 'summary'
|
||||
? null
|
||||
: 'agent_credit_line_settlement',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{start: string, end: string}
|
||||
*/
|
||||
private function resolvePeriod(int $periodId, Request $request): array
|
||||
{
|
||||
if ($periodId > 0) {
|
||||
$row = DB::table('settlement_periods')->where('id', $periodId)->first();
|
||||
abort_if($row === null, 404);
|
||||
|
||||
return [
|
||||
'start' => (string) $row->period_start,
|
||||
'end' => (string) $row->period_end,
|
||||
];
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'period_start' => ['required_with:period_end', 'date'],
|
||||
'period_end' => ['required_with:period_start', 'date', 'after_or_equal:period_start'],
|
||||
]);
|
||||
|
||||
$start = $request->query('period_start');
|
||||
$end = $request->query('period_end');
|
||||
if ($start && $end) {
|
||||
return ['start' => (string) $start, 'end' => (string) $end];
|
||||
}
|
||||
|
||||
$now = now();
|
||||
|
||||
return [
|
||||
'start' => $now->copy()->subDays(7)->toDateTimeString(),
|
||||
'end' => $now->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,10 @@ use App\Support\ApiResponse;
|
||||
use App\Models\SettlementBatch;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\AdminDrawResponsePolicy;
|
||||
use App\Support\AdminScopePolicy;
|
||||
use App\Support\ApiMessage;
|
||||
|
||||
/**
|
||||
* GET /api/v1/admin/draws/{draw}/finance-summary — 单期投注/派彩汇总(客服/财务视角,PRD §15.4)。
|
||||
@@ -23,6 +26,17 @@ final class AdminDrawFinanceSummaryController extends Controller
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if(! $admin instanceof AdminUser, 401);
|
||||
|
||||
if (! AdminDrawResponsePolicy::canViewDrawFinance($admin)) {
|
||||
return ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.permission_denied',
|
||||
ErrorCode::AdminForbidden->value,
|
||||
null,
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
$scope = AdminScopePolicy::resolveContext($request, $admin);
|
||||
|
||||
$drawId = (int) $draw->id;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Draw;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Draw;
|
||||
use App\Models\TicketItem;
|
||||
@@ -10,6 +9,8 @@ use App\Models\TicketOrder;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Support\AdminApiList;
|
||||
use App\Support\AdminScopeContext;
|
||||
use App\Support\AdminDrawApiPresenter;
|
||||
use App\Support\AdminDrawResponsePolicy;
|
||||
use App\Support\AdminScopePolicy;
|
||||
use App\Services\LotterySettings;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -44,19 +45,30 @@ final class AdminDrawIndexController extends Controller
|
||||
/** @var LengthAwarePaginator $paginator */
|
||||
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
||||
|
||||
$statsByDrawId = $this->aggregateListStats(
|
||||
$paginator->getCollection()->pluck('id')->map(fn ($id) => (int) $id)->all(),
|
||||
$scope,
|
||||
);
|
||||
$statsByDrawId = AdminDrawResponsePolicy::canViewDrawFinance($admin)
|
||||
? $this->aggregateListStats(
|
||||
$paginator->getCollection()->pluck('id')->map(fn ($id) => (int) $id)->all(),
|
||||
$scope,
|
||||
)
|
||||
: [];
|
||||
|
||||
return AdminApiList::jsonWith($paginator, fn (Draw $row) => $this->row($row, $statsByDrawId), [
|
||||
'schedule' => [
|
||||
'timezone' => LotterySettings::drawTimezone(),
|
||||
'interval_minutes' => LotterySettings::drawIntervalMinutes(),
|
||||
'betting_window_seconds' => LotterySettings::drawBettingWindowSeconds(),
|
||||
'close_before_draw_seconds' => LotterySettings::drawCloseBeforeDrawSeconds(),
|
||||
return AdminApiList::jsonWith(
|
||||
$paginator,
|
||||
fn (Draw $row): array => AdminDrawApiPresenter::listRow(
|
||||
$row,
|
||||
$statsByDrawId[(int) $row->id] ?? null,
|
||||
$admin,
|
||||
),
|
||||
[
|
||||
'schedule' => [
|
||||
'timezone' => LotterySettings::drawTimezone(),
|
||||
'interval_minutes' => LotterySettings::drawIntervalMinutes(),
|
||||
'betting_window_seconds' => LotterySettings::drawBettingWindowSeconds(),
|
||||
'close_before_draw_seconds' => LotterySettings::drawCloseBeforeDrawSeconds(),
|
||||
],
|
||||
'capabilities' => AdminDrawResponsePolicy::capabilities($admin),
|
||||
],
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,38 +141,4 @@ final class AdminDrawIndexController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{total_bet_minor: int, total_payout_minor: int, profit_loss_minor: int}> $statsByDrawId
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function row(Draw $draw, array $statsByDrawId): array
|
||||
{
|
||||
$stats = $statsByDrawId[(int) $draw->id] ?? [
|
||||
'total_bet_minor' => 0,
|
||||
'total_payout_minor' => 0,
|
||||
'profit_loss_minor' => 0,
|
||||
];
|
||||
|
||||
return [
|
||||
'id' => (int) $draw->id,
|
||||
'draw_no' => $draw->draw_no,
|
||||
'business_date' => $draw->business_date instanceof Carbon
|
||||
? $draw->business_date->format('Y-m-d')
|
||||
: (string) $draw->business_date,
|
||||
'sequence_no' => (int) $draw->sequence_no,
|
||||
'status' => $draw->status,
|
||||
'start_time' => $draw->start_time?->toIso8601String(),
|
||||
'close_time' => $draw->close_time?->toIso8601String(),
|
||||
'draw_time' => $draw->draw_time?->toIso8601String(),
|
||||
'cooling_end_time' => $draw->cooling_end_time?->toIso8601String(),
|
||||
'result_source' => $draw->result_source,
|
||||
'current_result_version' => (int) $draw->current_result_version,
|
||||
'settle_version' => (int) $draw->settle_version,
|
||||
'is_reopened' => (bool) $draw->is_reopened,
|
||||
'total_bet_minor' => $stats['total_bet_minor'],
|
||||
'total_payout_minor' => $stats['total_payout_minor'],
|
||||
'profit_loss_minor' => $stats['profit_loss_minor'],
|
||||
'updated_at' => $draw->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,56 +4,46 @@ namespace App\Http\Controllers\Api\V1\Admin\Draw;
|
||||
|
||||
use App\Models\Draw;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Models\DrawResultItem;
|
||||
use App\Models\DrawResultBatch;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminDrawApiPresenter;
|
||||
use App\Support\AdminDrawResponsePolicy;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
|
||||
/**
|
||||
* GET /api/v1/admin/draws/{draw}/result-batches — 开奖批次与号码(审核/结果核对)。
|
||||
*/
|
||||
final class AdminDrawResultBatchesIndexController extends Controller
|
||||
{
|
||||
public function __invoke(Draw $draw): JsonResponse
|
||||
public function __invoke(Request $request, Draw $draw): JsonResponse
|
||||
{
|
||||
$batches = $draw->resultBatches()
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$manage = AdminDrawResponsePolicy::canManageDrawResults($admin);
|
||||
|
||||
$query = $draw->resultBatches()
|
||||
->with(['items' => function ($q): void {
|
||||
$q->orderBy('prize_type')->orderBy('prize_index');
|
||||
}])
|
||||
->orderByDesc('result_version')
|
||||
->get();
|
||||
->orderByDesc('result_version');
|
||||
|
||||
if (! $manage) {
|
||||
$query->where('status', DrawResultBatchStatus::Published->value);
|
||||
}
|
||||
|
||||
$batches = $query->get();
|
||||
|
||||
return ApiResponse::success([
|
||||
'draw_id' => (int) $draw->id,
|
||||
'draw_no' => $draw->draw_no,
|
||||
'draw_status' => $draw->status,
|
||||
'batches' => $batches->map(fn (DrawResultBatch $b) => $this->serializeBatch($b))->all(),
|
||||
'capabilities' => AdminDrawResponsePolicy::capabilities($admin),
|
||||
'batches' => $batches
|
||||
->map(fn (DrawResultBatch $b): array => AdminDrawApiPresenter::resultBatch($b, $admin))
|
||||
->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function serializeBatch(DrawResultBatch $batch): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) $batch->id,
|
||||
'result_version' => (int) $batch->result_version,
|
||||
'source_type' => $batch->source_type,
|
||||
'rng_seed_hash' => $batch->rng_seed_hash,
|
||||
'status' => $batch->status,
|
||||
'created_by' => $batch->created_by,
|
||||
'confirmed_by' => $batch->confirmed_by,
|
||||
'confirmed_at' => $batch->confirmed_at?->toIso8601String(),
|
||||
'created_at' => $batch->created_at?->toIso8601String(),
|
||||
'updated_at' => $batch->updated_at?->toIso8601String(),
|
||||
'items' => $batch->items->map(fn (DrawResultItem $item) => [
|
||||
'prize_type' => $item->prize_type,
|
||||
'prize_index' => (int) $item->prize_index,
|
||||
'number_4d' => $item->number_4d,
|
||||
'suffix_3d' => $item->suffix_3d,
|
||||
'suffix_2d' => $item->suffix_2d,
|
||||
'head_digit' => $item->head_digit,
|
||||
'tail_digit' => $item->tail_digit,
|
||||
])->values()->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Draw;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Draw;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
use App\Support\AdminDrawApiPresenter;
|
||||
use App\Services\Draw\DrawHallSnapshotBuilder;
|
||||
|
||||
/**
|
||||
@@ -19,41 +19,13 @@ final class AdminDrawShowController extends Controller
|
||||
private readonly DrawHallSnapshotBuilder $hallPreview,
|
||||
) {}
|
||||
|
||||
public function __invoke(Draw $draw): JsonResponse
|
||||
public function __invoke(Request $request, Draw $draw): JsonResponse
|
||||
{
|
||||
$nowUtc = now()->utc();
|
||||
$batchCounts = [
|
||||
'total' => $draw->resultBatches()->count(),
|
||||
'pending_review' => $draw->resultBatches()
|
||||
->where('status', DrawResultBatchStatus::PendingReview->value)
|
||||
->count(),
|
||||
'published' => $draw->resultBatches()
|
||||
->where('status', DrawResultBatchStatus::Published->value)
|
||||
->count(),
|
||||
];
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
return ApiResponse::success([
|
||||
'id' => (int) $draw->id,
|
||||
'draw_no' => $draw->draw_no,
|
||||
'business_date' => $draw->business_date instanceof Carbon
|
||||
? $draw->business_date->format('Y-m-d')
|
||||
: (string) $draw->business_date,
|
||||
'sequence_no' => (int) $draw->sequence_no,
|
||||
/** 数据库当期状态(权威) */
|
||||
'status' => $draw->status,
|
||||
/** 与玩家大厅 snapshot 对齐的展示态(未跑 tick 时可能与 status 不一致) */
|
||||
'hall_preview_status' => $this->hallPreview->effectiveHallDisplayStatus($draw, $nowUtc),
|
||||
'start_time' => $draw->start_time?->toIso8601String(),
|
||||
'close_time' => $draw->close_time?->toIso8601String(),
|
||||
'draw_time' => $draw->draw_time?->toIso8601String(),
|
||||
'cooling_end_time' => $draw->cooling_end_time?->toIso8601String(),
|
||||
'result_source' => $draw->result_source,
|
||||
'current_result_version' => (int) $draw->current_result_version,
|
||||
'settle_version' => (int) $draw->settle_version,
|
||||
'is_reopened' => (bool) $draw->is_reopened,
|
||||
'created_at' => $draw->created_at?->toIso8601String(),
|
||||
'updated_at' => $draw->updated_at?->toIso8601String(),
|
||||
'result_batch_counts' => $batchCounts,
|
||||
]);
|
||||
return ApiResponse::success(
|
||||
AdminDrawApiPresenter::show($draw, $admin, $this->hallPreview),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Support\ApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AgentNode;
|
||||
use App\Support\AdminIntegrationSiteAccess;
|
||||
use App\Support\AdminIntegrationSitePresenter;
|
||||
|
||||
@@ -16,9 +17,18 @@ final class AdminIntegrationSiteIndexController extends Controller
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$items = AdminIntegrationSiteAccess::queryFor($admin)
|
||||
->get()
|
||||
->map(static fn ($site): array => AdminIntegrationSitePresenter::listItem($site))
|
||||
$sites = AdminIntegrationSiteAccess::queryFor($admin)->get();
|
||||
$rootSiteIds = AgentNode::query()
|
||||
->where('depth', 0)
|
||||
->whereIn('admin_site_id', $sites->pluck('id'))
|
||||
->pluck('admin_site_id')
|
||||
->flip();
|
||||
|
||||
$items = $sites
|
||||
->map(static fn ($site): array => AdminIntegrationSitePresenter::listItem(
|
||||
$site,
|
||||
isset($rootSiteIds[(int) $site->id]),
|
||||
))
|
||||
->all();
|
||||
|
||||
return ApiResponse::success(['items' => $items]);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Integration;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Middleware\RecordAdminApiAudit;
|
||||
use App\Models\AdminSite;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Support\AdminIntegrationSiteAccess;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/** GET /api/v1/admin/integration-sites/{admin_site}/secrets */
|
||||
final class AdminIntegrationSiteSecretsController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, AdminSite $admin_site): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
if (! AdminIntegrationSiteAccess::canAccess($admin, $admin_site)) {
|
||||
return ApiMessage::errorResponse($request, 'admin.site_access_denied', ErrorCode::AdminForbidden->value, null, 403);
|
||||
}
|
||||
|
||||
if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('integration.site.manage')) {
|
||||
return ApiMessage::errorResponse($request, 'admin.permission_denied', ErrorCode::AdminForbidden->value, null, 403);
|
||||
}
|
||||
|
||||
$sso = $admin_site->decryptedSsoJwtSecret();
|
||||
$wallet = $admin_site->decryptedWalletApiKey();
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
$request,
|
||||
moduleCode: 'integration',
|
||||
actionCode: 'reveal_secrets',
|
||||
targetType: 'admin_site',
|
||||
targetId: (string) $admin_site->id,
|
||||
afterJson: ['code' => $admin_site->code],
|
||||
);
|
||||
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||
|
||||
return ApiResponse::success([
|
||||
'sso_jwt_secret' => is_string($sso) ? $sso : '',
|
||||
'wallet_api_key' => is_string($wallet) ? $wallet : '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Integration;
|
||||
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AuditLogger;
|
||||
@@ -11,8 +10,6 @@ use App\Services\Integration\IntegrationSiteService;
|
||||
use App\Support\AdminIntegrationSitePresenter;
|
||||
use App\Http\Requests\Admin\AdminIntegrationSiteStoreRequest;
|
||||
use App\Http\Middleware\RecordAdminApiAudit;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\ApiMessage;
|
||||
|
||||
final class AdminIntegrationSiteStoreController extends Controller
|
||||
{
|
||||
@@ -23,19 +20,6 @@ final class AdminIntegrationSiteStoreController extends Controller
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
if (! $admin->isSuperAdmin()) {
|
||||
return ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.integration_site_store_deprecated',
|
||||
ErrorCode::AdminForbidden->value,
|
||||
['hint' => 'Use POST /api/v1/admin/agent-lines to provision a new agent line.'],
|
||||
403,
|
||||
)->withHeaders([
|
||||
'Deprecation' => 'true',
|
||||
'Link' => '</api/v1/admin/agent-lines>; rel="successor-version"',
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $service->create($request->validated());
|
||||
$site = $result['site'];
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Player;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\TicketOrder;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Support\ApiResponse;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminSiteScope;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/** DELETE /api/v1/admin/players/{player} */
|
||||
final class AdminPlayerDestroyController extends Controller
|
||||
@@ -26,16 +27,15 @@ final class AdminPlayerDestroyController extends Controller
|
||||
|
||||
$hasWallets = Player::query()
|
||||
->whereKey($player->getKey())
|
||||
->whereHas('wallets', static fn (HasMany $q) => $q->whereRaw('balance != 0'))
|
||||
->whereHas('wallets', static fn (Builder $q) => $q->where('balance', '!=', 0))
|
||||
->exists();
|
||||
|
||||
if ($hasWallets) {
|
||||
return ApiMessage::errorResponse($request, 'admin.player_wallet_balance_blocks_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
|
||||
$hasTickets = Player::query()
|
||||
->whereKey($player->getKey())
|
||||
->whereHas('ticketOrders')
|
||||
$hasTickets = TicketOrder::query()
|
||||
->where('player_id', $player->getKey())
|
||||
->exists();
|
||||
|
||||
if ($hasTickets) {
|
||||
|
||||
@@ -37,7 +37,11 @@ final class AdminPlayerIndexController extends Controller
|
||||
$term = '%'.addcslashes($keyword, '%_\\').'%';
|
||||
$q->where(static function ($sub) use ($term): void {
|
||||
$sub->where('site_player_id', 'like', $term)
|
||||
->orWhere('username', 'like', $term);
|
||||
->orWhere('username', 'like', $term)
|
||||
->orWhere('nickname', 'like', $term);
|
||||
if (ctype_digit($keyword)) {
|
||||
$sub->orWhere('id', (int) $keyword);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ use App\Models\AgentNode;
|
||||
use App\Services\Agent\AgentProfileService;
|
||||
use App\Services\Agent\RebateLimitValidator;
|
||||
use App\Services\Player\PlayerCreditService;
|
||||
use App\Support\PlayerAuthSource;
|
||||
use App\Support\PlayerFundingMode;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/** POST /api/v1/admin/players */
|
||||
final class AdminPlayerStoreController extends Controller
|
||||
@@ -46,15 +50,44 @@ final class AdminPlayerStoreController extends Controller
|
||||
return ApiMessage::errorResponse($request, 'admin.player_create_site_forbidden', ErrorCode::AdminForbidden->value, null, 403);
|
||||
}
|
||||
|
||||
$isNative = $request->filled('password');
|
||||
$sitePlayerId = (string) ($request->validated('site_player_id') ?? '');
|
||||
if ($isNative) {
|
||||
$sitePlayerId = $sitePlayerId !== ''
|
||||
? $sitePlayerId
|
||||
: 'native:'.Str::lower(Str::ulid());
|
||||
}
|
||||
|
||||
if ($sitePlayerId === '') {
|
||||
return ApiMessage::errorResponse($request, 'admin.player_site_player_id_required', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
|
||||
$exists = Player::query()
|
||||
->where('site_code', $request->validated('site_code'))
|
||||
->where('site_player_id', $request->validated('site_player_id'))
|
||||
->where('site_code', $siteCode)
|
||||
->where('site_player_id', $sitePlayerId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return ApiMessage::errorResponse($request, 'admin.player_already_registered', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
|
||||
if ($isNative) {
|
||||
$username = trim((string) $request->validated('username', ''));
|
||||
if ($username === '') {
|
||||
return ApiMessage::errorResponse($request, 'admin.player_native_username_required', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
|
||||
$usernameTaken = Player::query()
|
||||
->where('site_code', $siteCode)
|
||||
->where('username', $username)
|
||||
->where('auth_source', PlayerAuthSource::LOTTERY_NATIVE)
|
||||
->exists();
|
||||
|
||||
if ($usernameTaken) {
|
||||
return ApiMessage::errorResponse($request, 'admin.player_username_taken', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
}
|
||||
|
||||
$agentNodeId = $admin->isSuperAdmin()
|
||||
? $this->resolveAgentNodeIdForSuperAdmin($request->validated('agent_node_id'), $siteCode)
|
||||
: $admin->primaryAgentNodeId();
|
||||
@@ -85,33 +118,58 @@ final class AdminPlayerStoreController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => $request->validated('site_code'),
|
||||
'agent_node_id' => $agentNodeId,
|
||||
'site_player_id' => $request->validated('site_player_id'),
|
||||
'username' => $request->validated('username'),
|
||||
'nickname' => $request->validated('nickname'),
|
||||
'default_currency' => $request->validated('default_currency', 'NPR'),
|
||||
'status' => $request->validated('status', 0),
|
||||
]);
|
||||
$creditLimit = $request->has('credit_limit')
|
||||
? (int) $request->input('credit_limit', 0)
|
||||
: ($isNative ? 0 : 0);
|
||||
|
||||
if ($request->has('credit_limit')) {
|
||||
$playerCreditService->upsertAccount($player, [
|
||||
'credit_limit' => (int) $request->input('credit_limit', 0),
|
||||
]);
|
||||
}
|
||||
$player = \Illuminate\Support\Facades\DB::transaction(function () use (
|
||||
$agent,
|
||||
$agentProfileService,
|
||||
$playerCreditService,
|
||||
$request,
|
||||
$isNative,
|
||||
$siteCode,
|
||||
$agentNodeId,
|
||||
$sitePlayerId,
|
||||
$creditLimit,
|
||||
): Player {
|
||||
$agentProfileService->assertMayIncreasePlayerCredit($agent, $creditLimit);
|
||||
|
||||
if ($request->has('rebate_rate')) {
|
||||
\Illuminate\Support\Facades\DB::table('player_rebate_profiles')->insert([
|
||||
'player_id' => $player->id,
|
||||
'game_type' => '*',
|
||||
'inherit_from_agent' => false,
|
||||
'rebate_rate' => (float) $request->input('rebate_rate', 0),
|
||||
'extra_rebate_rate' => (float) $request->input('extra_rebate_rate', 0),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
$player = Player::query()->create([
|
||||
'site_code' => $siteCode,
|
||||
'agent_node_id' => $agentNodeId,
|
||||
'site_player_id' => $sitePlayerId,
|
||||
'auth_source' => $isNative ? PlayerAuthSource::LOTTERY_NATIVE : PlayerAuthSource::MAIN_SITE_SSO,
|
||||
'funding_mode' => $isNative ? PlayerFundingMode::CREDIT : PlayerFundingMode::WALLET,
|
||||
'username' => $isNative ? trim((string) $request->validated('username')) : $request->validated('username'),
|
||||
'password_hash' => $isNative ? Hash::make((string) $request->validated('password')) : null,
|
||||
'nickname' => $request->validated('nickname'),
|
||||
'default_currency' => $request->validated('default_currency', 'NPR'),
|
||||
'status' => $request->validated('status', 0),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($request->has('credit_limit') || $isNative) {
|
||||
$playerCreditService->upsertAccount($player, [
|
||||
'credit_limit' => $creditLimit,
|
||||
]);
|
||||
}
|
||||
|
||||
$agentProfileService->refreshAllocatedCredit($agent);
|
||||
|
||||
if ($request->has('rebate_rate')) {
|
||||
\Illuminate\Support\Facades\DB::table('player_rebate_profiles')->insert([
|
||||
'player_id' => $player->id,
|
||||
'game_type' => '*',
|
||||
'inherit_from_agent' => false,
|
||||
'rebate_rate' => (float) $request->input('rebate_rate', 0),
|
||||
'extra_rebate_rate' => (float) $request->input('extra_rebate_rate', 0),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $player;
|
||||
});
|
||||
|
||||
return ApiResponse::success(PlayerApiPresenter::listItem($player))->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -2,20 +2,31 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Player;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminSiteScope;
|
||||
use App\Support\PlayerApiPresenter;
|
||||
use App\Http\Requests\Admin\AdminPlayerUpdateRequest;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\Player;
|
||||
use App\Services\Agent\AgentProfileService;
|
||||
use App\Services\Agent\RebateLimitValidator;
|
||||
use App\Services\Player\PlayerCreditService;
|
||||
use App\Services\Player\PlayerRebateProfileService;
|
||||
use App\Support\AdminSiteScope;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Support\PlayerApiPresenter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** PUT /api/v1/admin/players/{player} */
|
||||
final class AdminPlayerUpdateController extends Controller
|
||||
{
|
||||
public function __invoke(AdminPlayerUpdateRequest $request, Player $player): JsonResponse
|
||||
{
|
||||
public function __invoke(
|
||||
AdminPlayerUpdateRequest $request,
|
||||
Player $player,
|
||||
AgentProfileService $agentProfileService,
|
||||
PlayerCreditService $playerCreditService,
|
||||
RebateLimitValidator $rebateLimitValidator,
|
||||
PlayerRebateProfileService $rebateProfileService,
|
||||
): JsonResponse {
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
@@ -29,7 +40,57 @@ final class AdminPlayerUpdateController extends Controller
|
||||
$data['status'] = (int) $data['status'];
|
||||
}
|
||||
|
||||
$player->fill(array_filter($data, static fn ($v) => $v !== ''));
|
||||
$agent = $player->agent_node_id !== null
|
||||
? AgentNode::query()->find((int) $player->agent_node_id)
|
||||
: null;
|
||||
|
||||
if ($agent !== null && $request->has('rebate_rate')) {
|
||||
$rebateLimitValidator->assertPlayerRebateWithinAgent(
|
||||
$agent,
|
||||
(float) $request->input('rebate_rate', 0),
|
||||
(float) $request->input('extra_rebate_rate', 0),
|
||||
);
|
||||
}
|
||||
|
||||
if ($request->has('credit_limit') && $agent !== null) {
|
||||
$newLimit = (int) $request->input('credit_limit', 0);
|
||||
$creditRow = DB::table('player_credit_accounts')->where('player_id', $player->id)->first();
|
||||
$previous = (int) ($creditRow->credit_limit ?? 0);
|
||||
$usedCredit = (int) ($creditRow->used_credit ?? 0) + (int) ($creditRow->frozen_credit ?? 0);
|
||||
$agentProfileService->adjustPlayerCreditAllocation($agent, $previous, $newLimit, $usedCredit);
|
||||
$playerCreditService->upsertAccount($player, ['credit_limit' => $newLimit]);
|
||||
$agentProfileService->refreshAllocatedCredit($agent);
|
||||
unset($data['credit_limit']);
|
||||
}
|
||||
|
||||
if ($request->has('rebate_rate')) {
|
||||
DB::table('player_rebate_profiles')->updateOrInsert(
|
||||
['player_id' => $player->id, 'game_type' => '*'],
|
||||
[
|
||||
'inherit_from_agent' => false,
|
||||
'rebate_rate' => (float) $request->input('rebate_rate', 0),
|
||||
'extra_rebate_rate' => (float) $request->input('extra_rebate_rate', 0),
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
],
|
||||
);
|
||||
unset($data['rebate_rate'], $data['extra_rebate_rate']);
|
||||
}
|
||||
|
||||
if ($request->has('rebate_profiles') && $agent !== null) {
|
||||
$rebateProfileService->syncProfiles($player->id, $agent, $request->input('rebate_profiles', []));
|
||||
unset($data['rebate_profiles']);
|
||||
}
|
||||
|
||||
if ($request->has('risk_tags')) {
|
||||
$player->risk_tags = array_values(array_unique(array_filter(
|
||||
array_map('strval', $request->input('risk_tags', [])),
|
||||
)));
|
||||
unset($data['risk_tags']);
|
||||
}
|
||||
|
||||
unset($data['rebate_profiles']);
|
||||
$player->fill(array_filter($data, static fn ($v) => $v !== '' && ! is_array($v)));
|
||||
$player->save();
|
||||
|
||||
return ApiResponse::success(PlayerApiPresenter::listItem($player));
|
||||
|
||||
@@ -32,7 +32,7 @@ final class AdminReportRebateCommissionController extends Controller
|
||||
$scope,
|
||||
);
|
||||
|
||||
return AdminApiList::json($paginator, static function (object $row): array {
|
||||
$response = AdminApiList::json($paginator, static function (object $row): array {
|
||||
return [
|
||||
'play_code' => (string) $row->play_code,
|
||||
'total_rebate_minor' => (int) $row->total_rebate_minor,
|
||||
@@ -40,5 +40,13 @@ final class AdminReportRebateCommissionController extends Controller
|
||||
'ticket_item_count' => (int) $row->ticket_item_count,
|
||||
];
|
||||
});
|
||||
|
||||
$payload = $response->getData(true);
|
||||
if (is_array($payload)) {
|
||||
$payload['disclaimer'] = 'wallet_instant_rebate_not_agent_period_settlement';
|
||||
$response->setData($payload);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminAccountScopeGuard;
|
||||
use App\Support\AdminRoleApiPresenter;
|
||||
use App\Support\PlatformSystemRoles;
|
||||
|
||||
final class AdminRoleDestroyController extends Controller
|
||||
{
|
||||
@@ -19,8 +20,8 @@ final class AdminRoleDestroyController extends Controller
|
||||
{
|
||||
AdminAccountScopeGuard::assertSystemRole($admin_role);
|
||||
|
||||
if ($admin_role->slug === AdminRole::ROLE_SUPER_ADMIN) {
|
||||
return ApiMessage::errorResponse($request, 'admin.role_cannot_delete_super_admin', ErrorCode::ValidationFailed->value, null, 422);
|
||||
if (PlatformSystemRoles::isFixedSlug((string) $admin_role->slug)) {
|
||||
return ApiMessage::errorResponse($request, 'admin.role_builtin_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
if ((bool) $admin_role->is_system) {
|
||||
return ApiMessage::errorResponse($request, 'admin.role_builtin_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminRoleApiPresenter;
|
||||
use App\Support\PlatformSystemRoles;
|
||||
|
||||
final class AdminRoleIndexController extends Controller
|
||||
{
|
||||
@@ -14,6 +15,7 @@ final class AdminRoleIndexController extends Controller
|
||||
{
|
||||
$roles = AdminRole::query()
|
||||
->where('scope_type', AdminRole::SCOPE_SYSTEM)
|
||||
->whereIn('slug', PlatformSystemRoles::fixedSlugs())
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
@@ -9,8 +9,11 @@ use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminPermissionInheritance;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\AdminAccountScopeGuard;
|
||||
use App\Support\AdminRoleApiPresenter;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Support\PlatformSystemRoles;
|
||||
use App\Http\Requests\Admin\AdminRolePermissionSyncRequest;
|
||||
|
||||
final class AdminRolePermissionSyncController extends Controller
|
||||
@@ -19,6 +22,16 @@ final class AdminRolePermissionSyncController extends Controller
|
||||
{
|
||||
AdminAccountScopeGuard::assertSystemRole($admin_role);
|
||||
|
||||
if ($admin_role->slug === PlatformSystemRoles::SLUG_SUPER_ADMIN) {
|
||||
return ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.role_super_admin_permissions_fixed',
|
||||
ErrorCode::ValidationFailed->value,
|
||||
null,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
$slugs = AdminPermissionInheritance::expand(
|
||||
array_values(array_unique($request->validated('permission_slugs', []))),
|
||||
);
|
||||
|
||||
@@ -2,53 +2,22 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\User;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\ApiMessage;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminPermissionInheritance;
|
||||
use App\Support\AdminRoleApiPresenter;
|
||||
use App\Http\Requests\Admin\AdminRoleStoreRequest;
|
||||
|
||||
final class AdminRoleStoreController extends Controller
|
||||
{
|
||||
public function __invoke(AdminRoleStoreRequest $request): JsonResponse
|
||||
{
|
||||
$permissionSlugs = AdminPermissionInheritance::expand(
|
||||
array_values(array_unique($request->validated('permission_slugs', []))),
|
||||
);
|
||||
|
||||
$role = DB::transaction(function () use ($request, $permissionSlugs): AdminRole {
|
||||
$role = AdminRole::query()->create([
|
||||
'slug' => $request->validated('slug'),
|
||||
'code' => $request->validated('slug'),
|
||||
'name' => $request->validated('name'),
|
||||
'description' => $request->validated('description'),
|
||||
'status' => $request->validated('status', 1),
|
||||
'is_system' => false,
|
||||
'sort_order' => 0,
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
'owner_agent_id' => null,
|
||||
'delegated_from_role_id' => null,
|
||||
]);
|
||||
$role->syncLegacyPermissionSlugs($permissionSlugs);
|
||||
|
||||
return $role->fresh();
|
||||
});
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$request->lotteryAdmin(),
|
||||
return ApiMessage::errorResponse(
|
||||
$request,
|
||||
'system',
|
||||
'admin_role.create',
|
||||
'admin_role',
|
||||
(string) $role->id,
|
||||
'admin.platform_roles_fixed',
|
||||
ErrorCode::ValidationFailed->value,
|
||||
null,
|
||||
AdminRoleApiPresenter::item($role),
|
||||
422,
|
||||
);
|
||||
|
||||
return ApiResponse::success(AdminRoleApiPresenter::item($role));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
namespace App\Http\Controllers\Api\V1\Admin\User;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Services\AuditLogger;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminAccountScopeGuard;
|
||||
use App\Support\AdminRoleApiPresenter;
|
||||
use App\Support\PlatformSystemRoles;
|
||||
use App\Http\Requests\Admin\AdminRoleUpdateRequest;
|
||||
|
||||
final class AdminRoleUpdateController extends Controller
|
||||
@@ -17,6 +20,16 @@ final class AdminRoleUpdateController extends Controller
|
||||
{
|
||||
AdminAccountScopeGuard::assertSystemRole($admin_role);
|
||||
|
||||
if ($admin_role->slug === PlatformSystemRoles::SLUG_SUPER_ADMIN) {
|
||||
return ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.role_super_admin_metadata_fixed',
|
||||
ErrorCode::ValidationFailed->value,
|
||||
null,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
$before = AdminRoleApiPresenter::item($admin_role);
|
||||
|
||||
$payload = [];
|
||||
|
||||
@@ -8,6 +8,7 @@ use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminAccountScopeGuard;
|
||||
use App\Support\AdminUserApiPresenter;
|
||||
use App\Support\AdminPlatformUserSiteGuard;
|
||||
use App\Http\Requests\Admin\AdminUserRoleSyncRequest;
|
||||
|
||||
/** PUT /api/v1/admin/admin-users/{admin_user}/roles */
|
||||
@@ -15,10 +16,16 @@ final class AdminUserRoleSyncController extends Controller
|
||||
{
|
||||
public function __invoke(AdminUserRoleSyncRequest $request, AdminUser $admin_user): JsonResponse
|
||||
{
|
||||
/** @var AdminUser $actor */
|
||||
$actor = $request->lotteryAdmin();
|
||||
|
||||
AdminAccountScopeGuard::assertPlatformAccount($admin_user);
|
||||
|
||||
$siteId = (int) $request->validated('admin_site_id');
|
||||
AdminPlatformUserSiteGuard::assertActorCanAssignSite($actor, $siteId);
|
||||
|
||||
$slugs = array_values(array_unique($request->validated('role_slugs')));
|
||||
$admin_user->syncSystemRoleSlugs($slugs);
|
||||
$admin_user->syncSystemRoleSlugsForSite($siteId, $slugs);
|
||||
|
||||
$admin_user->load('roles');
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminUserApiPresenter;
|
||||
use App\Support\AdminPlatformUserSiteGuard;
|
||||
use App\Http\Requests\Admin\AdminUserStoreRequest;
|
||||
|
||||
/**
|
||||
@@ -28,8 +29,10 @@ final class AdminUserStoreController extends Controller
|
||||
: null;
|
||||
|
||||
$roleSlugs = array_values(array_unique($request->validated('role_slugs')));
|
||||
$siteId = (int) $request->validated('admin_site_id');
|
||||
AdminPlatformUserSiteGuard::assertActorCanAssignSite($actor, $siteId);
|
||||
|
||||
$user = DB::transaction(function () use ($request, $email, $roleSlugs): AdminUser {
|
||||
$user = DB::transaction(function () use ($request, $email, $roleSlugs, $siteId): AdminUser {
|
||||
$created = AdminUser::query()->create([
|
||||
'username' => $request->validated('username'),
|
||||
'name' => $request->validated('nickname'),
|
||||
@@ -37,7 +40,7 @@ final class AdminUserStoreController extends Controller
|
||||
'password' => $request->validated('password'),
|
||||
'status' => $request->validated('status', 0),
|
||||
]);
|
||||
$created->syncSystemRoleSlugs($roleSlugs);
|
||||
$created->syncSystemRoleSlugsForSite($siteId, $roleSlugs);
|
||||
|
||||
return $created;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Wallet;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\WalletTxn;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Support\PaginationTrait;
|
||||
@@ -9,8 +10,10 @@ use Illuminate\Http\JsonResponse;
|
||||
use App\Support\AdminScopePolicy;
|
||||
use App\Support\AgentNodeApiPresenter;
|
||||
use App\Support\CurrencyFormatter;
|
||||
use App\Support\PlayerFundingMode;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\WalletTransactionListRequest;
|
||||
use App\Services\Wallet\PlayerLedgerLogsService;
|
||||
|
||||
/**
|
||||
* 后台:彩票钱包流水列表 {@see wallet_txns}。
|
||||
@@ -33,6 +36,10 @@ final class WalletTransactionListController extends Controller
|
||||
|
||||
private const ALLOWED_STATUS = ['posted', 'pending_reconcile', 'reversed'];
|
||||
|
||||
public function __construct(
|
||||
private readonly PlayerLedgerLogsService $ledgerLogs,
|
||||
) {}
|
||||
|
||||
public function __invoke(WalletTransactionListRequest $request): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
@@ -44,6 +51,20 @@ final class WalletTransactionListController extends Controller
|
||||
$perPage = $this->perPage($request, 'per_page', 10, 100);
|
||||
$page = $this->page($request);
|
||||
|
||||
if (! empty($validated['player_id'])) {
|
||||
$player = Player::query()->find((int) $validated['player_id']);
|
||||
if ($player !== null && PlayerFundingMode::usesCredit($player)) {
|
||||
$credit = $this->ledgerLogs->listForAdminPlayer(
|
||||
$player,
|
||||
$page,
|
||||
$perPage,
|
||||
isset($validated['biz_type']) ? (string) $validated['biz_type'] : null,
|
||||
);
|
||||
|
||||
return ApiResponse::success($credit);
|
||||
}
|
||||
}
|
||||
|
||||
$query = WalletTxn::query()
|
||||
->with([
|
||||
'player:id,site_code,site_player_id,username,nickname,agent_node_id',
|
||||
@@ -141,6 +162,9 @@ final class WalletTransactionListController extends Controller
|
||||
'remark' => $t->remark,
|
||||
'created_at' => $t->created_at?->toIso8601String(),
|
||||
'updated_at' => $t->updated_at?->toIso8601String(),
|
||||
'ledger_source' => 'wallet_txn',
|
||||
'funding_mode' => $p?->funding_mode,
|
||||
'auth_source' => $p?->auth_source,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ final class MeController extends Controller
|
||||
'id' => $player->id,
|
||||
'site_code' => $player->site_code,
|
||||
'site_player_id' => $player->site_player_id,
|
||||
'auth_source' => $player->auth_source,
|
||||
'funding_mode' => $player->funding_mode,
|
||||
'username' => $player->username,
|
||||
'nickname' => $player->nickname,
|
||||
'default_currency' => $player->default_currency,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Player;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Player\PlayerAuthLoginRequest;
|
||||
use App\Services\Player\PlayerNativeAuthService;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Exceptions\PlayerAuthenticationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/** POST /api/v1/player/auth/login — 代理线下玩家账号密码登录 */
|
||||
final class PlayerAuthLoginController extends Controller
|
||||
{
|
||||
public function __invoke(PlayerAuthLoginRequest $request, PlayerNativeAuthService $auth): JsonResponse
|
||||
{
|
||||
try {
|
||||
$data = $auth->login(
|
||||
(string) $request->validated('site_code'),
|
||||
(string) $request->validated('username'),
|
||||
(string) $request->validated('password'),
|
||||
);
|
||||
} catch (PlayerAuthenticationException $e) {
|
||||
return ApiResponse::error(
|
||||
$e->getMessage(),
|
||||
$e->lotteryCode,
|
||||
null,
|
||||
$e->httpStatus,
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse::success($data);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,12 @@ use App\Lottery\ErrorCode;
|
||||
use App\Models\PlayerWallet;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Support\PlayerFundingMode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Support\CurrencyResolver;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Support\CreditAmountScale;
|
||||
use App\Support\CurrencyFormatter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Wallet\HttpMainSiteWalletBalanceClient;
|
||||
@@ -44,6 +47,37 @@ final class WalletBalanceController extends Controller
|
||||
return $currencyCode;
|
||||
}
|
||||
|
||||
if (PlayerFundingMode::usesCredit($player)) {
|
||||
$credit = DB::table('player_credit_accounts')->where('player_id', $player->id)->first();
|
||||
$limitMajor = (int) ($credit->credit_limit ?? 0);
|
||||
$usedMajor = (int) ($credit->used_credit ?? 0);
|
||||
$frozenMajor = (int) ($credit->frozen_credit ?? 0);
|
||||
$availableMajor = max(0, $limitMajor - $usedMajor - $frozenMajor);
|
||||
|
||||
$limitMinor = CreditAmountScale::majorToMinor($limitMajor, $currencyCode);
|
||||
$usedMinor = CreditAmountScale::majorToMinor($usedMajor, $currencyCode);
|
||||
$frozenMinor = CreditAmountScale::majorToMinor($frozenMajor, $currencyCode);
|
||||
$availableMinor = CreditAmountScale::majorToMinor($availableMajor, $currencyCode);
|
||||
|
||||
return ApiResponse::success([
|
||||
'balance' => $limitMinor,
|
||||
'balance_formatted' => CurrencyFormatter::fromMinor($limitMinor),
|
||||
'available_balance' => $availableMinor,
|
||||
'available_balance_formatted' => CurrencyFormatter::fromMinor($availableMinor),
|
||||
'credit_limit' => $limitMinor,
|
||||
'used_credit' => $usedMinor,
|
||||
'credit_line_mode' => true,
|
||||
'funding_mode' => PlayerFundingMode::CREDIT,
|
||||
'auth_source' => $player->auth_source,
|
||||
'main_balance' => null,
|
||||
'main_balance_formatted' => null,
|
||||
'currency_code' => $currencyCode,
|
||||
'wallet_type' => self::WALLET_TYPE_LOTTERY,
|
||||
'frozen_balance' => $frozenMinor,
|
||||
'frozen_balance_formatted' => CurrencyFormatter::fromMinor($frozenMinor),
|
||||
]);
|
||||
}
|
||||
|
||||
$wallet = PlayerWallet::query()->firstOrCreate(
|
||||
[
|
||||
'player_id' => $player->id,
|
||||
@@ -69,6 +103,9 @@ final class WalletBalanceController extends Controller
|
||||
'balance_formatted' => CurrencyFormatter::fromMinor($balance),
|
||||
'available_balance' => $available,
|
||||
'available_balance_formatted' => CurrencyFormatter::fromMinor($available),
|
||||
'credit_line_mode' => false,
|
||||
'funding_mode' => PlayerFundingMode::WALLET,
|
||||
'auth_source' => $player->auth_source,
|
||||
'main_balance' => $mainBalance,
|
||||
'main_balance_formatted' => $mainBalance !== null
|
||||
? CurrencyFormatter::fromMinor($mainBalance)
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Wallet;
|
||||
|
||||
use App\Models\WalletTxn;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\TransferOrder;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\TransferOrder;
|
||||
use App\Support\PaginationTrait;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Support\CurrencyFormatter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Wallet\PlayerLedgerLogsService;
|
||||
|
||||
/**
|
||||
* PRD §10.1.1:`GET /api/v1/wallet/logs` — 钱包流水。
|
||||
* PRD §10.1.1:`GET /api/v1/wallet/logs` — 钱包/信用流水(按玩家资金模式分表)。
|
||||
*
|
||||
* Query:`page`、`size`(每页条数,默认 20)、`type`(逗号分隔:transfer_in,transfer_out,bet,prize,refund,reversal)
|
||||
*/
|
||||
@@ -21,15 +20,9 @@ final class WalletLogsController extends Controller
|
||||
{
|
||||
use PaginationTrait;
|
||||
|
||||
/** PRD 对外类型 → 本地 biz_type */
|
||||
private const TYPE_TO_BIZ = [
|
||||
'transfer_in' => ['transfer_in'],
|
||||
'transfer_out' => ['transfer_out'],
|
||||
'refund' => ['transfer_out_refund'],
|
||||
'reversal' => ['reversal', 'bet_reverse'],
|
||||
'bet' => ['bet_deduct', 'bet'],
|
||||
'prize' => ['settle_payout', 'prize', 'jackpot_manual_payout'],
|
||||
];
|
||||
public function __construct(
|
||||
private readonly PlayerLedgerLogsService $ledgerLogs,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
@@ -38,45 +31,21 @@ final class WalletLogsController extends Controller
|
||||
|
||||
$perPage = $this->perPage($request, 'size', 20, 100);
|
||||
$page = $this->page($request);
|
||||
|
||||
$currencyCode = strtoupper(trim((string) $request->query('currency', '')));
|
||||
$typeFilter = (string) $request->query('type', '');
|
||||
|
||||
$pendingPayload = $this->pendingReconcilePayload((int) $player->id, $currencyCode);
|
||||
|
||||
$bizFilter = $this->resolveBizTypeFilter((string) $request->query('type', ''));
|
||||
|
||||
if (is_array($bizFilter) && $bizFilter === []) {
|
||||
return ApiResponse::success([
|
||||
'items' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'pending_reconcile' => $pendingPayload,
|
||||
]);
|
||||
}
|
||||
|
||||
$query = WalletTxn::query()
|
||||
->where('player_id', $player->id)
|
||||
->with('wallet')
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($currencyCode !== '') {
|
||||
$query->whereHas('wallet', fn ($q) => $q->where('currency_code', $currencyCode));
|
||||
}
|
||||
|
||||
if ($bizFilter !== null) {
|
||||
$query->whereIn('biz_type', $bizFilter);
|
||||
}
|
||||
|
||||
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
$items = $paginator->getCollection()->map(fn (WalletTxn $txn) => $this->formatTxn($txn));
|
||||
$result = $this->ledgerLogs->listForPlayerApi($player, $page, $perPage, $currencyCode, $typeFilter);
|
||||
|
||||
return ApiResponse::success([
|
||||
'items' => $items,
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'items' => $result['items'],
|
||||
'total' => $result['total'],
|
||||
'page' => $result['page'],
|
||||
'per_page' => $result['per_page'],
|
||||
'ledger_source' => $result['ledger_source'],
|
||||
'funding_mode' => $result['funding_mode'],
|
||||
'auth_source' => $result['auth_source'],
|
||||
'pending_reconcile' => $pendingPayload,
|
||||
]);
|
||||
}
|
||||
@@ -97,84 +66,6 @@ final class WalletLogsController extends Controller
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>|null null 表示不过滤;空列表表示过滤后无合法 type(结果应为空)
|
||||
*/
|
||||
private function resolveBizTypeFilter(string $raw): ?array
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = array_filter(array_map('trim', explode(',', $raw)));
|
||||
if ($parts === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$biz = [];
|
||||
foreach ($parts as $p) {
|
||||
$key = Str::lower($p);
|
||||
if (! isset(self::TYPE_TO_BIZ[$key])) {
|
||||
continue;
|
||||
}
|
||||
foreach (self::TYPE_TO_BIZ[$key] as $b) {
|
||||
$biz[] = $b;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($biz));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatTxn(WalletTxn $txn): array
|
||||
{
|
||||
$currency = $txn->wallet?->currency_code ?? '';
|
||||
$amount = (int) $txn->amount;
|
||||
$balanceAfter = (int) $txn->balance_after;
|
||||
|
||||
return [
|
||||
'log_id' => $txn->txn_no,
|
||||
'type' => $this->bizToPublicType((string) $txn->biz_type),
|
||||
'biz_type' => $txn->biz_type,
|
||||
'amount' => $this->signedAmount($txn),
|
||||
'amount_formatted' => CurrencyFormatter::fromMinor($amount),
|
||||
'amount_abs' => $amount,
|
||||
'amount_abs_formatted' => CurrencyFormatter::fromMinor($amount),
|
||||
'direction' => (int) $txn->direction === 1 ? 'in' : 'out',
|
||||
'currency_code' => $currency,
|
||||
'balance_after' => $balanceAfter,
|
||||
'balance_after_formatted' => CurrencyFormatter::fromMinor($balanceAfter),
|
||||
'ref_id' => $txn->biz_no,
|
||||
'idempotent_key' => $txn->idempotent_key,
|
||||
'external_ref_no' => $txn->external_ref_no,
|
||||
'status' => $txn->status,
|
||||
'remark' => $txn->remark,
|
||||
'created_at' => $txn->created_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
private function bizToPublicType(string $biz): string
|
||||
{
|
||||
return match ($biz) {
|
||||
'transfer_out_refund' => 'refund',
|
||||
'bet_deduct', 'bet' => 'bet',
|
||||
'bet_reverse' => 'reversal',
|
||||
'settle_payout', 'prize', 'jackpot_manual_payout' => 'prize',
|
||||
'reversal' => 'reversal',
|
||||
default => $biz,
|
||||
};
|
||||
}
|
||||
|
||||
private function signedAmount(WalletTxn $txn): int
|
||||
{
|
||||
$a = (int) $txn->amount;
|
||||
|
||||
return (int) $txn->direction === 1 ? $a : -$a;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user