feat: 增强代理和玩家管理功能

- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。
- 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。
- 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。
- 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。
- 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
This commit is contained in:
2026-06-04 18:00:50 +08:00
parent 96545f87f6
commit a44679665d
183 changed files with 10054 additions and 857 deletions

View File

@@ -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);
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -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,
]);
}
}

View File

@@ -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,
]);
}
}

View File

@@ -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);

View File

@@ -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}";
}
}

View File

@@ -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]);
}
}

View File

@@ -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,
]);
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -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),
]);
}
}

View File

@@ -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));
}
}

View File

@@ -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(),
];
}
}