- Added new section in AGENTS.md detailing learned workspace facts for better understanding of settlement processes. - Updated AgentNodeDestroyController to remove unnecessary checks for admin users. - Enhanced AgentSettlement controllers to assert permissions for finance adjustments and bill operations. - Improved query scopes in AgentSettlement services to ensure proper data access based on admin roles. - Refactored methods in SettlementPartyEnrichment for better bill row enrichment and data handling. - Introduced new methods in AdminAgentSettlementScope for managing agent node visibility and finance adjustments.
200 lines
7.2 KiB
PHP
200 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\AgentSettlement;
|
|
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/** 开账弹窗:建议账期与日历标记(已有账期 / 待入账 / 未结清)。 */
|
|
final class SettlementPeriodOpenHintsService
|
|
{
|
|
/**
|
|
* @return array{
|
|
* suggested_start: string,
|
|
* suggested_end: string,
|
|
* occupied_period_dates: list<string>,
|
|
* pending_activity_dates: list<string>,
|
|
* unpaid_bill_dates: list<string>
|
|
* }
|
|
*/
|
|
public function hints(int $adminSiteId): array
|
|
{
|
|
$siteCode = (string) DB::table('admin_sites')->where('id', $adminSiteId)->value('code');
|
|
if ($siteCode === '') {
|
|
return $this->emptyHints();
|
|
}
|
|
|
|
$periodRows = DB::table('settlement_periods')
|
|
->where('admin_site_id', $adminSiteId)
|
|
->orderBy('period_start')
|
|
->get(['period_start', 'period_end', 'status']);
|
|
|
|
$occupiedPeriodDates = [];
|
|
foreach ($periodRows as $row) {
|
|
foreach ($this->expandPeriodToUtcDays((string) $row->period_start, (string) $row->period_end) as $day) {
|
|
$occupiedPeriodDates[$day] = true;
|
|
}
|
|
}
|
|
|
|
$lastPeriod = DB::table('settlement_periods')
|
|
->where('admin_site_id', $adminSiteId)
|
|
->whereIn('status', ['closed', 'completed'])
|
|
->orderByDesc('period_end')
|
|
->first();
|
|
|
|
$pendingActivityDates = DB::table('share_ledger as sl')
|
|
->join('players as p', 'p.id', '=', 'sl.player_id')
|
|
->where('p.site_code', $siteCode)
|
|
->whereNull('sl.settlement_period_id')
|
|
->whereNull('sl.reversal_of_id')
|
|
->selectRaw('DATE(sl.settled_at) as activity_day')
|
|
->groupBy('activity_day')
|
|
->orderBy('activity_day')
|
|
->pluck('activity_day')
|
|
->map(static fn ($day): string => (string) $day)
|
|
->values()
|
|
->all();
|
|
|
|
$unpaidPeriodRows = DB::table('settlement_periods as sp')
|
|
->where('sp.admin_site_id', $adminSiteId)
|
|
->whereIn('sp.status', ['closed', 'completed'])
|
|
->whereExists(function ($query): void {
|
|
$query->selectRaw('1')
|
|
->from('settlement_bills as sb')
|
|
->whereColumn('sb.settlement_period_id', 'sp.id')
|
|
->where('sb.unpaid_amount', '>', 0)
|
|
->whereIn('sb.status', ['pending_confirm', 'confirmed', 'partial_paid', 'overdue']);
|
|
})
|
|
->orderBy('sp.period_start')
|
|
->get(['sp.period_start', 'sp.period_end']);
|
|
|
|
$unpaidBillDates = [];
|
|
foreach ($unpaidPeriodRows as $row) {
|
|
foreach ($this->expandPeriodToUtcDays((string) $row->period_start, (string) $row->period_end) as $day) {
|
|
$unpaidBillDates[$day] = true;
|
|
}
|
|
}
|
|
|
|
$suggested = $this->suggestRange($lastPeriod, $pendingActivityDates, $occupiedPeriodDates);
|
|
|
|
return [
|
|
'suggested_start' => $suggested['start'],
|
|
'suggested_end' => $suggested['end'],
|
|
'occupied_period_dates' => array_keys($occupiedPeriodDates),
|
|
'pending_activity_dates' => $pendingActivityDates,
|
|
'unpaid_bill_dates' => array_keys($unpaidBillDates),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $pendingActivityDates UTC `Y-m-d`
|
|
* @param array<string, true> $occupiedPeriodDates
|
|
* @return array{start: string, end: string}
|
|
*/
|
|
private function suggestRange(?object $lastPeriod, array $pendingActivityDates, array $occupiedPeriodDates): array
|
|
{
|
|
$lastEndDay = $lastPeriod !== null
|
|
? Carbon::parse((string) $lastPeriod->period_end)->utc()->startOfDay()
|
|
: null;
|
|
|
|
$freePending = array_values(array_filter(
|
|
$pendingActivityDates,
|
|
static fn (string $day): bool => ! isset($occupiedPeriodDates[$day]),
|
|
));
|
|
|
|
if ($freePending !== []) {
|
|
$minDay = Carbon::parse($freePending[0])->utc()->startOfDay();
|
|
$maxDay = Carbon::parse($freePending[array_key_last($freePending)])->utc()->startOfDay();
|
|
$startDay = $lastEndDay !== null
|
|
? ($lastEndDay->copy()->addDay()->lessThanOrEqualTo($minDay) ? $lastEndDay->copy()->addDay() : $minDay)
|
|
: $minDay;
|
|
|
|
$candidate = [
|
|
'start' => $startDay->format('Y-m-d'),
|
|
'end' => $maxDay->format('Y-m-d'),
|
|
];
|
|
|
|
return $this->withoutOccupiedOverlap($candidate, $occupiedPeriodDates);
|
|
}
|
|
|
|
if ($lastEndDay !== null) {
|
|
$startDay = $lastEndDay->copy()->addDay();
|
|
$endDay = Carbon::now('UTC')->subDay()->startOfDay();
|
|
if ($endDay->lessThan($startDay)) {
|
|
return ['start' => '', 'end' => ''];
|
|
}
|
|
|
|
return $this->withoutOccupiedOverlap([
|
|
'start' => $startDay->format('Y-m-d'),
|
|
'end' => $endDay->format('Y-m-d'),
|
|
], $occupiedPeriodDates);
|
|
}
|
|
|
|
return ['start' => '', 'end' => ''];
|
|
}
|
|
|
|
/**
|
|
* @param array{start: string, end: string} $candidate
|
|
* @param array<string, true> $occupiedPeriodDates
|
|
* @return array{start: string, end: string}
|
|
*/
|
|
private function withoutOccupiedOverlap(array $candidate, array $occupiedPeriodDates): array
|
|
{
|
|
if ($candidate['start'] === '' || $candidate['end'] === '') {
|
|
return ['start' => '', 'end' => ''];
|
|
}
|
|
|
|
if ($this->rangeOverlapsOccupied($candidate['start'], $candidate['end'], $occupiedPeriodDates)) {
|
|
return ['start' => '', 'end' => ''];
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, true> $occupiedPeriodDates
|
|
*/
|
|
private function rangeOverlapsOccupied(string $startYmd, string $endYmd, array $occupiedPeriodDates): bool
|
|
{
|
|
$cursor = Carbon::parse($startYmd)->utc()->startOfDay();
|
|
$end = Carbon::parse($endYmd)->utc()->startOfDay();
|
|
|
|
while ($cursor->lessThanOrEqualTo($end)) {
|
|
if (isset($occupiedPeriodDates[$cursor->format('Y-m-d')])) {
|
|
return true;
|
|
}
|
|
$cursor->addDay();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** @return list<string> 站点本地日历 `Y-m-d`(东八区,与后台开账日期选择一致) */
|
|
private function expandPeriodToUtcDays(string $periodStart, string $periodEnd): array
|
|
{
|
|
$dates = [];
|
|
$tz = 'Asia/Shanghai';
|
|
$cursor = Carbon::parse($periodStart)->timezone($tz)->startOfDay();
|
|
$end = Carbon::parse($periodEnd)->timezone($tz)->startOfDay();
|
|
|
|
while ($cursor->lessThanOrEqualTo($end)) {
|
|
$dates[] = $cursor->format('Y-m-d');
|
|
$cursor->addDay();
|
|
}
|
|
|
|
return $dates;
|
|
}
|
|
|
|
/** @return array{suggested_start: string, suggested_end: string, occupied_period_dates: list<string>, pending_activity_dates: list<string>, unpaid_bill_dates: list<string>} */
|
|
private function emptyHints(): array
|
|
{
|
|
return [
|
|
'suggested_start' => '',
|
|
'suggested_end' => '',
|
|
'occupied_period_dates' => [],
|
|
'pending_activity_dates' => [],
|
|
'unpaid_bill_dates' => [],
|
|
];
|
|
}
|
|
}
|