feat: enhance agent settlement features and improve data access controls
- 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.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
<?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' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user