完成店铺台账每日余额来源、日期筛选、预留标准和连续3天预警。 Co-authored-by: 92451 <924518135@qq.com> Co-committed-by: 92451 <924518135@qq.com>
1082 lines
46 KiB
PHP
1082 lines
46 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\View;
|
||
|
||
use App\Business\WithdrawalBalanceAlertSynchronizer;
|
||
use App\Business\WithdrawalReserveStandard;
|
||
use App\Utilities\FinanceLedgerAccountStatus;
|
||
use App\Utilities\FinanceLedgerAccountShouldKeep;
|
||
use App\Utilities\FinanceLedgerAccountType;
|
||
use App\Utilities\FinanceLedgerAccess;
|
||
use Illuminate\Database\Query\Builder;
|
||
use Illuminate\Pagination\LengthAwarePaginator;
|
||
use Illuminate\Pagination\Paginator;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Modules\EcommerceFinance\Services\FinanceLedgerShopDirectory;
|
||
use Modules\EcommerceFinance\Services\StoreReceivingBillBalanceSnapshotSynchronizer;
|
||
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
|
||
|
||
/**
|
||
* 财务台账页面数据。
|
||
*
|
||
* 所有页签一次性读取,页签切换只做前端显示/隐藏;各页签使用独立的分页
|
||
* 参数,避免在同一 URL 上互相覆盖。
|
||
*/
|
||
class FinanceLedgerPageData
|
||
{
|
||
public const TABS = [
|
||
'accounts' => ['title' => '账户总表', 'icon' => 'account_balance'],
|
||
'shops' => ['title' => '店铺台账', 'icon' => 'storefront'],
|
||
'flows' => ['title' => '每日资金流水', 'icon' => 'waterfall_chart'],
|
||
'transactions' => ['title' => '动账明细', 'icon' => 'receipt_long'],
|
||
'mapping' => ['title' => '账户映射', 'icon' => 'alt_route'],
|
||
'sources' => ['title' => '数据来源', 'icon' => 'database'],
|
||
];
|
||
|
||
private const ACCOUNT_FILTER_EMPTY = '__empty__';
|
||
|
||
public function __construct(
|
||
private readonly FinanceLedgerShopDirectory $shopDirectory,
|
||
private readonly JushuitanShopBalanceRepository $shopBalances,
|
||
) {
|
||
}
|
||
|
||
public function page(?string $tab = null): array
|
||
{
|
||
$tab = (string) (request('tab') ?: $tab);
|
||
$tab = array_key_exists($tab, self::TABS) ? $tab : 'accounts';
|
||
$openingDate = (string) config('finance_ledger.opening_date', '2026-10-17');
|
||
|
||
$accounts = $this->accounts();
|
||
$shops = $this->shops();
|
||
$availableBreakdown = $this->availableBreakdown($shops);
|
||
$flows = $this->flows();
|
||
$transactions = $this->transactions();
|
||
|
||
return [
|
||
'view' => 'business.pages.financial-ledger.ledger',
|
||
'ledgerTab' => $tab,
|
||
'ledgerTabs' => self::TABS,
|
||
'ledgerOpeningDate' => $openingDate,
|
||
'ledgerAccounts' => $accounts['rows'],
|
||
'ledgerAccountSummary' => $accounts['summary'],
|
||
'ledgerShops' => $shops['rows'],
|
||
'ledgerShopSummary' => $shops['summary'],
|
||
'ledgerShopBalanceDate' => $shops['balance_date'] ?? null,
|
||
'ledgerShopBalanceDates' => $shops['balance_dates'] ?? [],
|
||
'ledgerAvailableBreakdown' => $availableBreakdown,
|
||
'ledgerFlows' => $flows['rows'],
|
||
'ledgerFlowSummary' => $flows['summary'],
|
||
'ledgerTransactions' => $transactions['rows'],
|
||
'ledgerTransactionSummary' => $transactions['summary'],
|
||
'ledgerMapping' => $this->mapping(),
|
||
'ledgerSourceSummary' => $this->sourceSummary(),
|
||
'ledgerMeta' => $this->meta(),
|
||
'ledgerAccountOptions' => $this->accountOptions(),
|
||
'ledgerManualTransactionAccountOptions' => $this->manualTransactionAccountOptions(),
|
||
'ledgerTransactionAccountHeaderOptions' => $this->transactionAccountHeaderOptions(),
|
||
'ledgerTransactionDateHeaderOptions' => $this->transactionDateHeaderOptions(),
|
||
'ledgerAccountFilterOptions' => $this->accountFilterOptions(),
|
||
'ledgerCompanyOptions' => $this->companyOptions(),
|
||
'ledgerAccountStatusOptions' => FinanceLedgerAccountStatus::OPTIONS,
|
||
'ledgerAccountPlanOptions' => $this->accountPlanOptions(),
|
||
'ledgerAccountKeepOptions' => FinanceLedgerAccountShouldKeep::OPTIONS,
|
||
'ledgerCanEditMapping' => FinanceLedgerAccess::canEditMapping(user()),
|
||
'ledgerCanEditAccountStatus' => FinanceLedgerAccess::canEditAccountStatus(user()),
|
||
'ledgerCanEditAccountType' => FinanceLedgerAccess::canEditAccountType(user()),
|
||
];
|
||
}
|
||
|
||
private function accounts(): array
|
||
{
|
||
$query = $this->db()->table('fin_accounts as a');
|
||
$keyword = trim((string) request('accounts_q', ''));
|
||
if ($keyword !== '') {
|
||
$query->where(function (Builder $builder) use ($keyword): void {
|
||
foreach (['a.company_name', 'a.account_short', 'a.account_no', 'a.owner'] as $column) {
|
||
$builder->orWhere($column, 'like', '%' . $keyword . '%');
|
||
}
|
||
});
|
||
}
|
||
|
||
$accountShort = trim((string) request('accounts_short', ''));
|
||
if ($accountShort !== '') {
|
||
$query->where('a.account_short', 'like', '%' . $accountShort . '%');
|
||
}
|
||
|
||
$status = trim((string) request('accounts_status', ''));
|
||
if ($status !== '' && array_key_exists($status, FinanceLedgerAccountStatus::OPTIONS)) {
|
||
if ($status === FinanceLedgerAccountStatus::NORMAL) {
|
||
$nonNormalStatuses = array_values(array_diff(
|
||
array_keys(FinanceLedgerAccountStatus::OPTIONS),
|
||
[FinanceLedgerAccountStatus::NORMAL],
|
||
));
|
||
$query->whereNotIn(
|
||
'a.account_short',
|
||
$this->db()->table('fin_account_statuses')
|
||
->select('account_short')
|
||
->whereIn('status', $nonNormalStatuses),
|
||
);
|
||
} else {
|
||
$query->whereIn(
|
||
'a.account_short',
|
||
$this->db()->table('fin_account_statuses')
|
||
->select('account_short')
|
||
->where('status', $status),
|
||
);
|
||
}
|
||
}
|
||
|
||
$region = trim((string) request('accounts_region', ''));
|
||
if ($region !== '') {
|
||
if ($region === self::ACCOUNT_FILTER_EMPTY) {
|
||
$query->where(function (Builder $builder): void {
|
||
$builder->whereNull('a.region')->orWhere('a.region', '');
|
||
});
|
||
} else {
|
||
$query->where('a.region', $region);
|
||
}
|
||
}
|
||
|
||
$accountType = trim((string) request('accounts_type', ''));
|
||
if ($accountType !== '') {
|
||
$query->where(function (Builder $effectiveType) use ($accountType): void {
|
||
$effectiveType->where(function (Builder $baseType) use ($accountType): void {
|
||
$baseType->whereNotIn(
|
||
'a.account_short',
|
||
$this->db()->table('fin_account_type_overrides')->select('account_short'),
|
||
);
|
||
if ($accountType === self::ACCOUNT_FILTER_EMPTY) {
|
||
$baseType->where(function (Builder $blank): void {
|
||
$blank->whereNull('a.account_type')->orWhere('a.account_type', '');
|
||
});
|
||
} else {
|
||
$baseType->where('a.account_type', $accountType);
|
||
}
|
||
});
|
||
|
||
$overrideType = $this->db()->table('fin_account_type_overrides')->select('account_short');
|
||
if ($accountType === self::ACCOUNT_FILTER_EMPTY) {
|
||
$overrideType->where(function (Builder $blank): void {
|
||
$blank->whereNull('account_type')->orWhere('account_type', '');
|
||
});
|
||
} else {
|
||
$overrideType->where('account_type', $accountType);
|
||
}
|
||
$effectiveType->orWhereIn('a.account_short', $overrideType);
|
||
});
|
||
}
|
||
|
||
$balanceRange = trim((string) request('accounts_balance', ''));
|
||
if ($balanceRange === '0-5') {
|
||
$query->where('a.current_balance', '>=', 0)->where('a.current_balance', '<', 50000);
|
||
} elseif ($balanceRange === '5-20') {
|
||
$query->where('a.current_balance', '>=', 50000)->where('a.current_balance', '<', 200000);
|
||
} elseif ($balanceRange === '20-plus') {
|
||
$query->where('a.current_balance', '>=', 200000);
|
||
}
|
||
|
||
$headerColumns = $this->accountHeaderColumns();
|
||
$this->applyLedgerHeaderFilter($query, 'accounts', $headerColumns);
|
||
$summaryQuery = clone $query;
|
||
$summary = [
|
||
'count' => (clone $query)->count(),
|
||
'unretained' => FinanceLedgerAccountShouldKeep::applyUnretainedCondition(
|
||
clone $query,
|
||
'a.account_should_keep',
|
||
'a.company_plan',
|
||
)->count(),
|
||
'current_balance' => round((float) (clone $summaryQuery)->sum('a.current_balance'), 2),
|
||
'balance' => round((float) (clone $summaryQuery)->sum('a.daily_available'), 2),
|
||
'blocked' => round((float) (clone $summaryQuery)->sum('a.blocked_amount'), 2),
|
||
];
|
||
|
||
$sorted = $this->applyLedgerHeaderSort($query, 'accounts', $headerColumns);
|
||
if (! $sorted) {
|
||
$query->orderBy('a.company_name')->orderBy('a.account_short');
|
||
}
|
||
$rows = $query
|
||
->paginate(100, ['*'], 'accounts_page')
|
||
->withQueryString();
|
||
|
||
$accountShorts = $rows->pluck('account_short')->filter()->unique()->values()->all();
|
||
$statuses = $accountShorts === []
|
||
? collect()
|
||
: $this->db()->table('fin_account_statuses')
|
||
->whereIn('account_short', $accountShorts)
|
||
->pluck('status', 'account_short');
|
||
$typeOverrides = $accountShorts === []
|
||
? collect()
|
||
: $this->db()->table('fin_account_type_overrides')
|
||
->whereIn('account_short', $accountShorts)
|
||
->pluck('account_type', 'account_short');
|
||
$rows->getCollection()->transform(static function ($row) use ($statuses, $typeOverrides) {
|
||
$row->account_status = FinanceLedgerAccountStatus::normalize($statuses[$row->account_short] ?? null);
|
||
if ($typeOverrides->has($row->account_short)) {
|
||
$row->account_type = (string) ($typeOverrides->get($row->account_short) ?? '');
|
||
}
|
||
$row->account_should_keep = FinanceLedgerAccountShouldKeep::display(
|
||
$row->account_should_keep ?? null,
|
||
$row->company_plan ?? null,
|
||
);
|
||
|
||
return $row;
|
||
});
|
||
|
||
return ['rows' => $rows, 'summary' => $summary];
|
||
}
|
||
|
||
private function shops(): array
|
||
{
|
||
$directoryRows = $this->shopDirectory->rows();
|
||
|
||
if ($directoryRows === null) {
|
||
return $this->shopsFromLedgerTable();
|
||
}
|
||
|
||
$context = $this->storeLedgerBalanceContext($directoryRows);
|
||
|
||
return $this->shopsFromStoreReport($context['rows'], $context);
|
||
}
|
||
|
||
/**
|
||
* 店铺余额按日期读取专用快照。店铺收款账户账单有真实余额的平台
|
||
* 优先使用账单来源,其余平台由同步任务以聚水潭余额补足并明确标注。
|
||
*
|
||
* @param list<array<string,mixed>> $rows
|
||
* @return array{
|
||
* rows:list<array<string,mixed>>,balance_date:?string,
|
||
* balance_dates:list<string>,alert_count:int
|
||
* }
|
||
*/
|
||
private function storeLedgerBalanceContext(array $rows): array
|
||
{
|
||
$latestAllowed = now()->startOfDay()->subDay();
|
||
$balanceDates = $this->shopBalances->availableDates(
|
||
$latestAllowed,
|
||
StoreReceivingBillBalanceSnapshotSynchronizer::SOURCE_SCOPE,
|
||
);
|
||
$requestedDate = trim((string) request('shops_balance_date', ''));
|
||
$balanceDate = in_array($requestedDate, $balanceDates, true)
|
||
? $requestedDate
|
||
: ($balanceDates[0] ?? null);
|
||
|
||
$reserveStandards = [];
|
||
foreach ($rows as $index => $row) {
|
||
$reserveStandards[(int) ($row['id'] ?? $index)] = WithdrawalReserveStandard::amountFor(
|
||
(string) ($row['platform'] ?? ''),
|
||
(string) ($row['shop_name'] ?? ''),
|
||
);
|
||
$rows[$index]['shop_balance_source'] = ($row['shop_balance_date'] ?? null)
|
||
? StoreReceivingBillBalanceSnapshotSynchronizer::sourceLabel(
|
||
JushuitanShopBalanceRepository::DATA_SOURCE_JUSHUITAN,
|
||
)
|
||
: '未同步';
|
||
$rows[$index]['shop_balance_status'] = ($row['shop_balance_date'] ?? null) ? 'matched' : 'not_ready';
|
||
$rows[$index]['reserve_standard'] = $reserveStandards[(int) ($row['id'] ?? $index)];
|
||
$rows[$index]['balance_alert'] = '否';
|
||
$rows[$index]['balance_alert_message'] = null;
|
||
}
|
||
|
||
if ($balanceDate === null) {
|
||
return [
|
||
'rows' => $rows,
|
||
'balance_date' => null,
|
||
'balance_dates' => [],
|
||
'alert_count' => 0,
|
||
];
|
||
}
|
||
|
||
$stores = array_map(
|
||
static fn (array $row): object => (object) $row,
|
||
$rows,
|
||
);
|
||
$historyDates = WithdrawalBalanceAlertSynchronizer::datesEndingAt($balanceDate);
|
||
$matches = $this->shopBalances->matchesForStoresOnDates(
|
||
$stores,
|
||
$historyDates,
|
||
null,
|
||
StoreReceivingBillBalanceSnapshotSynchronizer::SOURCE_SCOPE,
|
||
);
|
||
$alerts = WithdrawalBalanceAlertSynchronizer::detect(
|
||
$stores,
|
||
$matches,
|
||
array_map(
|
||
static fn (?int $amount): ?int => $amount === null ? null : (int) $amount,
|
||
$reserveStandards,
|
||
),
|
||
$historyDates,
|
||
);
|
||
|
||
foreach ($rows as $index => $row) {
|
||
$id = (int) ($row['id'] ?? $index);
|
||
$match = $matches[$id][$balanceDate] ?? [
|
||
'status' => 'not_ready',
|
||
'balance' => null,
|
||
'data_source' => null,
|
||
'source_file_name' => null,
|
||
];
|
||
$matched = ($match['status'] ?? '') === 'matched' && ($match['balance'] ?? null) !== null;
|
||
$rows[$index]['shop_balance'] = $matched ? round((float) $match['balance'], 2) : null;
|
||
$rows[$index]['shop_balance_date'] = $matched ? $balanceDate : null;
|
||
$rows[$index]['shop_balance_status'] = (string) ($match['status'] ?? 'not_ready');
|
||
$rows[$index]['shop_balance_source'] = $matched
|
||
? StoreReceivingBillBalanceSnapshotSynchronizer::sourceLabel(
|
||
$match['data_source'] ?? null,
|
||
$match['source_file_name'] ?? null,
|
||
)
|
||
: '未同步';
|
||
$alert = $alerts[$id] ?? null;
|
||
$rows[$index]['balance_alert'] = $alert === null ? '否' : '是';
|
||
$rows[$index]['balance_alert_message'] = $alert['message'] ?? null;
|
||
}
|
||
|
||
return [
|
||
'rows' => $rows,
|
||
'balance_date' => $balanceDate,
|
||
'balance_dates' => $balanceDates,
|
||
'alert_count' => count($alerts),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param list<array<string,mixed>> $rows
|
||
* @param array{balance_date:?string,balance_dates:list<string>,alert_count:int}|array{} $context
|
||
* @return array{
|
||
* rows:LengthAwarePaginator,summary:array{count:int,balance:float,alert_count:int},
|
||
* overall_summary:array{count:int,balance:float},balance_date:?string,
|
||
* balance_dates:list<string>,alert_count:int
|
||
* }
|
||
*/
|
||
private function shopsFromStoreReport(array $rows, array $context = []): array
|
||
{
|
||
$overallSummary = [
|
||
'count' => count($rows),
|
||
'balance' => round(array_sum(array_map(
|
||
static fn (array $row): float => (float) ($row['shop_balance'] ?? 0),
|
||
$rows,
|
||
)), 2),
|
||
];
|
||
$keyword = trim((string) request('shops_q', ''));
|
||
$platform = trim((string) request('shops_platform', ''));
|
||
$company = trim((string) request('shops_company', ''));
|
||
|
||
$rows = array_values(array_filter(
|
||
$rows,
|
||
static function (array $row) use ($keyword, $platform, $company): bool {
|
||
if ($platform !== '' && trim((string) ($row['platform'] ?? '')) !== $platform) {
|
||
return false;
|
||
}
|
||
if ($company !== '' && mb_stripos((string) ($row['company_name'] ?? ''), $company) === false) {
|
||
return false;
|
||
}
|
||
if ($keyword === '') {
|
||
return true;
|
||
}
|
||
|
||
foreach (['shop_name', 'platform', 'company_name'] as $field) {
|
||
if (mb_stripos((string) ($row[$field] ?? ''), $keyword) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
},
|
||
));
|
||
|
||
$headerColumns = $this->shopHeaderColumns();
|
||
$rows = $this->filterAndSortHeaderRows($rows, 'shops', $headerColumns);
|
||
if (! isset($headerColumns[trim((string) request('shops_sort', ''))])) {
|
||
usort($rows, static fn (array $left, array $right): int => [
|
||
(string) ($left['company_name'] ?? ''),
|
||
(string) ($left['shop_name'] ?? ''),
|
||
] <=> [
|
||
(string) ($right['company_name'] ?? ''),
|
||
(string) ($right['shop_name'] ?? ''),
|
||
]);
|
||
}
|
||
|
||
$summary = [
|
||
'count' => count($rows),
|
||
'balance' => round(array_sum(array_map(
|
||
static fn (array $row): float => (float) ($row['shop_balance'] ?? 0),
|
||
$rows,
|
||
)), 2),
|
||
'alert_count' => count(array_filter(
|
||
$rows,
|
||
static fn (array $row): bool => ($row['balance_alert'] ?? '') === '是',
|
||
)),
|
||
];
|
||
$perPage = 100;
|
||
$currentPage = Paginator::resolveCurrentPage('shops_page');
|
||
$items = array_map(
|
||
static fn (array $row): object => (object) $row,
|
||
array_slice($rows, ($currentPage - 1) * $perPage, $perPage),
|
||
);
|
||
|
||
return [
|
||
'rows' => (new LengthAwarePaginator(
|
||
$items,
|
||
count($rows),
|
||
$perPage,
|
||
$currentPage,
|
||
[
|
||
'path' => Paginator::resolveCurrentPath(),
|
||
'query' => request()->query(),
|
||
'pageName' => 'shops_page',
|
||
],
|
||
))->withQueryString(),
|
||
'summary' => $summary,
|
||
'overall_summary' => $overallSummary,
|
||
'balance_date' => $context['balance_date'] ?? null,
|
||
'balance_dates' => $context['balance_dates'] ?? [],
|
||
'alert_count' => $summary['alert_count'],
|
||
];
|
||
}
|
||
|
||
/** @return array{rows:LengthAwarePaginator,summary:array{count:int,balance:float},overall_summary:array{count:int,balance:float}} */
|
||
private function shopsFromLedgerTable(): array
|
||
{
|
||
$baseQuery = $this->db()->table('fin_shops');
|
||
$overallSummary = [
|
||
'count' => (clone $baseQuery)->count(),
|
||
'balance' => round((float) (clone $baseQuery)->sum('shop_balance'), 2),
|
||
];
|
||
$query = clone $baseQuery;
|
||
foreach (['shops_q' => ['shop_name', 'platform', 'company_name'], 'shops_platform' => ['platform'], 'shops_company' => ['company_name']] as $param => $columns) {
|
||
$value = trim((string) request($param, ''));
|
||
if ($value === '') {
|
||
continue;
|
||
}
|
||
$query->where(function (Builder $builder) use ($columns, $value): void {
|
||
foreach ($columns as $column) {
|
||
$builder->orWhere($column, 'like', '%' . $value . '%');
|
||
}
|
||
});
|
||
}
|
||
|
||
$headerColumns = $this->shopHeaderColumns();
|
||
$this->applyLedgerHeaderFilter($query, 'shops', $headerColumns);
|
||
$summary = [
|
||
'count' => (clone $query)->count(),
|
||
'balance' => round((float) (clone $query)->sum('shop_balance'), 2),
|
||
'alert_count' => 0,
|
||
];
|
||
|
||
$sorted = $this->applyLedgerHeaderSort($query, 'shops', $headerColumns);
|
||
if (! $sorted) {
|
||
$query->orderBy('company_name')->orderBy('shop_name');
|
||
}
|
||
|
||
$rows = $query
|
||
->paginate(100, ['*'], 'shops_page')->withQueryString();
|
||
$rows->getCollection()->transform(static function ($row) {
|
||
$row->shop_balance_source = ($row->shop_balance_date ?? null)
|
||
? StoreReceivingBillBalanceSnapshotSynchronizer::sourceLabel(
|
||
JushuitanShopBalanceRepository::DATA_SOURCE_JUSHUITAN,
|
||
)
|
||
: '未同步';
|
||
$row->shop_balance_status = ($row->shop_balance_date ?? null) ? 'matched' : 'not_ready';
|
||
$row->reserve_standard = WithdrawalReserveStandard::amountFor(
|
||
(string) ($row->platform ?? ''),
|
||
(string) ($row->shop_name ?? ''),
|
||
);
|
||
$row->balance_alert = '否';
|
||
$row->balance_alert_message = null;
|
||
|
||
return $row;
|
||
});
|
||
|
||
return [
|
||
'rows' => $rows,
|
||
'summary' => $summary,
|
||
'overall_summary' => $overallSummary,
|
||
'balance_date' => $this->latestShopBalanceDate(),
|
||
'balance_dates' => $this->shopBalances->availableDates(
|
||
now()->startOfDay()->subDay(),
|
||
StoreReceivingBillBalanceSnapshotSynchronizer::SOURCE_SCOPE,
|
||
),
|
||
'alert_count' => 0,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 综合可用金额始终按全部账户和全部店铺计算,不受账户/店铺筛选影响。
|
||
*
|
||
* @return array{bank_available:float,shop_available:float,total:float,shop_balance_date:?string}
|
||
*/
|
||
private function availableBreakdown(array $shops): array
|
||
{
|
||
$bankAvailable = round((float) $this->db()->table('fin_accounts')->sum('daily_available'), 2);
|
||
$shopAvailable = round((float) ($shops['overall_summary']['balance'] ?? 0), 2);
|
||
|
||
return [
|
||
'bank_available' => $bankAvailable,
|
||
'shop_available' => $shopAvailable,
|
||
'total' => round($bankAvailable + $shopAvailable, 2),
|
||
'shop_balance_date' => $shops['balance_date'] ?? $this->latestShopBalanceDate(),
|
||
];
|
||
}
|
||
|
||
private function flows(): array
|
||
{
|
||
$query = $this->db()->table('fin_daily_flows');
|
||
foreach (['flows_date' => 'flow_date', 'flows_company' => 'company_name', 'flows_account' => 'account_short'] as $param => $column) {
|
||
$value = trim((string) request($param, ''));
|
||
if ($value !== '') {
|
||
$query->where($column, $param === 'flows_company' || $param === 'flows_account' ? 'like' : '=', $param === 'flows_company' || $param === 'flows_account' ? '%' . $value . '%' : $value);
|
||
}
|
||
}
|
||
|
||
$headerColumns = $this->flowHeaderColumns();
|
||
$this->applyLedgerHeaderFilter($query, 'flows', $headerColumns);
|
||
$summary = [
|
||
'count' => (clone $query)->count(),
|
||
'closing' => round((float) (clone $query)->sum('closing_balance'), 2),
|
||
'inflow' => round((float) (clone $query)->sum('inflow'), 2),
|
||
'outflow' => round((float) (clone $query)->sum('outflow'), 2),
|
||
];
|
||
|
||
$sorted = $this->applyLedgerHeaderSort($query, 'flows', $headerColumns);
|
||
if (! $sorted) {
|
||
$query->orderBy('flow_date')->orderBy('account_short');
|
||
}
|
||
|
||
return [
|
||
'rows' => $query
|
||
->paginate(100, ['*'], 'flows_page')->withQueryString(),
|
||
'summary' => $summary,
|
||
];
|
||
}
|
||
|
||
private function transactions(): array
|
||
{
|
||
$query = $this->transactionQuery();
|
||
$this->applyTransactionFilters($query);
|
||
|
||
$headerColumns = $this->transactionHeaderColumns();
|
||
$summary = $this->transactionSummaryMetrics($query);
|
||
$sorted = $this->applyLedgerHeaderSort($query, 'transactions', $headerColumns);
|
||
if (! $sorted) {
|
||
$query->orderByDesc('t.txn_date')->orderByDesc('t.id');
|
||
}
|
||
|
||
return [
|
||
'rows' => $query
|
||
->paginate(100, ['*'], 'txns_page')->withQueryString(),
|
||
'summary' => $summary,
|
||
];
|
||
}
|
||
|
||
public function transactionSummary(): array
|
||
{
|
||
$query = $this->transactionQuery();
|
||
$this->applyTransactionFilters($query);
|
||
|
||
return $this->transactionSummaryMetrics($query);
|
||
}
|
||
|
||
private function applyTransactionFilters(Builder $query): void
|
||
{
|
||
$keyword = trim((string) request('txns_q', ''));
|
||
if ($keyword !== '') {
|
||
$query->where(function (Builder $builder) use ($keyword): void {
|
||
$builder->where('t.detail', 'like', '%' . $keyword . '%')
|
||
->orWhere('t.approval_no', 'like', '%' . $keyword . '%')
|
||
->orWhere('t.account_short', 'like', '%' . $keyword . '%')
|
||
->orWhere('m.account_short', 'like', '%' . $keyword . '%');
|
||
});
|
||
}
|
||
$account = trim((string) request('txns_account', ''));
|
||
if ($account !== '') {
|
||
$effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort'];
|
||
$query->whereRaw($effectiveAccount . ' LIKE ?', ['%' . $account . '%']);
|
||
}
|
||
$from = trim((string) request('txns_from', ''));
|
||
if ($from !== '') {
|
||
$query->where('t.txn_date', '>=', $from);
|
||
}
|
||
$to = trim((string) request('txns_to', ''));
|
||
if ($to !== '') {
|
||
$query->where('t.txn_date', '<=', $to);
|
||
}
|
||
$source = trim((string) request('txns_source', ''));
|
||
if ($source !== '') {
|
||
$query->where('t.source', $source);
|
||
}
|
||
|
||
$this->applyLedgerHeaderFilter($query, 'transactions', $this->transactionHeaderColumns());
|
||
}
|
||
|
||
private function transactionSummaryMetrics(Builder $query): array
|
||
{
|
||
return [
|
||
'count' => (clone $query)->count(),
|
||
'income' => round((float) (clone $query)->sum('t.income'), 2),
|
||
'expense' => round((float) (clone $query)->sum('t.expense'), 2),
|
||
];
|
||
}
|
||
|
||
private function transactionQuery(): Builder
|
||
{
|
||
$effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort'];
|
||
|
||
return $this->db()->table('fin_transactions as t')
|
||
->leftJoin('fin_account_map as m', function ($join): void {
|
||
$join->on('m.source', '=', 't.source')->on('m.source_value', '=', 't.source_value');
|
||
})
|
||
->select([
|
||
't.id',
|
||
't.txn_date',
|
||
't.account_short',
|
||
't.detail',
|
||
't.income',
|
||
't.expense',
|
||
't.approval_no',
|
||
't.source',
|
||
't.source_value',
|
||
't.process_type',
|
||
't.row_key',
|
||
't.synced_at',
|
||
't.created_at',
|
||
't.updated_at',
|
||
])
|
||
->selectRaw($effectiveAccount . ' AS effective_account_short');
|
||
}
|
||
|
||
private function transactionAccountHeaderOptions(): array
|
||
{
|
||
$effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort'];
|
||
|
||
return $this->db()->table('fin_transactions as t')
|
||
->leftJoin('fin_account_map as m', function ($join): void {
|
||
$join->on('m.source', '=', 't.source')
|
||
->on('m.source_value', '=', 't.source_value');
|
||
})
|
||
->selectRaw($effectiveAccount . ' AS filter_value')
|
||
->distinct()
|
||
->orderBy('filter_value')
|
||
->pluck('filter_value')
|
||
->map(static fn (mixed $value): string => trim((string) $value))
|
||
->filter()
|
||
->unique()
|
||
->mapWithKeys(static fn (string $value): array => [$value => $value])
|
||
->all();
|
||
}
|
||
|
||
private function transactionDateHeaderOptions(): array
|
||
{
|
||
return $this->db()->table('fin_transactions')
|
||
->select('txn_date')
|
||
->distinct()
|
||
->orderByDesc('txn_date')
|
||
->pluck('txn_date')
|
||
->map(static fn (mixed $value): string => (string) $value)
|
||
->filter()
|
||
->unique()
|
||
->mapWithKeys(static fn (string $value): array => [$value => $value])
|
||
->all();
|
||
}
|
||
|
||
private function mapping(): array
|
||
{
|
||
$query = $this->db()->table('fin_account_map');
|
||
$source = trim((string) request('mapping_source', ''));
|
||
if ($source !== '') {
|
||
$query->where('source', $source);
|
||
}
|
||
$keyword = trim((string) request('mapping_q', ''));
|
||
if ($keyword !== '') {
|
||
$query->where(function (Builder $builder) use ($keyword): void {
|
||
$builder->where('source_value', 'like', '%' . $keyword . '%')
|
||
->orWhere('account_short', 'like', '%' . $keyword . '%');
|
||
});
|
||
}
|
||
if (request()->boolean('mapping_unmapped')) {
|
||
$query->where(function (Builder $builder): void {
|
||
$builder->whereNull('account_short')->orWhere('account_short', '');
|
||
});
|
||
}
|
||
|
||
$headerColumns = $this->mappingHeaderColumns();
|
||
$this->applyLedgerHeaderFilter($query, 'mapping', $headerColumns);
|
||
$total = (clone $query)->count();
|
||
$mapped = (clone $query)->whereNotNull('account_short')->where('account_short', '<>', '')->count();
|
||
$sorted = $this->applyLedgerHeaderSort($query, 'mapping', $headerColumns);
|
||
if (! $sorted) {
|
||
$query->orderByDesc('txn_count')->orderBy('source_value');
|
||
}
|
||
|
||
return [
|
||
'rows' => $query
|
||
->paginate(100, ['*'], 'mapping_page')->withQueryString(),
|
||
'total' => $total,
|
||
'mapped' => $mapped,
|
||
'all_total' => $this->db()->table('fin_account_map')->count(),
|
||
'all_mapped' => $this->db()->table('fin_account_map')->whereNotNull('account_short')->where('account_short', '<>', '')->count(),
|
||
'sources' => $this->db()->table('fin_account_map')->select('source')->distinct()->orderBy('source')->pluck('source'),
|
||
];
|
||
}
|
||
|
||
private function sourceSummary(): array
|
||
{
|
||
$rows = $this->db()->table('fin_transactions')
|
||
->selectRaw('source, process_type, COUNT(*) AS rows_count, SUM(income) AS income, SUM(expense) AS expense, MIN(txn_date) AS first_date, MAX(txn_date) AS last_date')
|
||
->groupBy('source', 'process_type')
|
||
->orderBy('source')
|
||
->orderByDesc('rows_count')
|
||
->get()
|
||
->map(static function ($row): array {
|
||
$data = (array) $row;
|
||
$data['date_range'] = trim((string) ($data['first_date'] ?? '') . ' ~ ' . (string) ($data['last_date'] ?? ''));
|
||
|
||
return $data;
|
||
})
|
||
->all();
|
||
|
||
return $this->filterAndSortHeaderRows($rows, 'sources', $this->sourceHeaderColumns());
|
||
}
|
||
|
||
private function meta(): array
|
||
{
|
||
return [
|
||
'accounts' => $this->db()->table('fin_accounts')->count(),
|
||
'shops' => $this->db()->table('fin_shops')->count(),
|
||
'companies' => $this->db()->table('fin_companies')->count(),
|
||
'opening_rows' => $this->db()->table('fin_account_opening')->count(),
|
||
'opening_sum' => round((float) $this->db()->table('fin_account_opening')->sum('opening_balance'), 2),
|
||
'transactions' => $this->db()->table('fin_transactions')->count(),
|
||
'income' => round((float) $this->db()->table('fin_transactions')->sum('income'), 2),
|
||
'expense' => round((float) $this->db()->table('fin_transactions')->sum('expense'), 2),
|
||
'flows' => $this->db()->table('fin_daily_flows')->count(),
|
||
'mapping_total' => $this->db()->table('fin_account_map')->count(),
|
||
'mapping_done' => $this->db()->table('fin_account_map')->whereNotNull('account_short')->where('account_short', '<>', '')->count(),
|
||
'last_synced_at' => $this->db()->table('fin_transactions')->max('synced_at'),
|
||
];
|
||
}
|
||
|
||
private function latestShopBalanceDate(): ?string
|
||
{
|
||
if (! Schema::connection($this->db()->getName())->hasTable(JushuitanShopBalanceRepository::TABLE)) {
|
||
return null;
|
||
}
|
||
|
||
$latest = $this->db()->table(JushuitanShopBalanceRepository::TABLE)
|
||
->where('source_scope', StoreReceivingBillBalanceSnapshotSynchronizer::SOURCE_SCOPE)
|
||
->where('balance_date', '<=', now()->startOfDay()->subDay()->toDateString())
|
||
->max('balance_date');
|
||
|
||
return $latest === null ? null : substr((string) $latest, 0, 10);
|
||
}
|
||
|
||
private function accountOptions(): array
|
||
{
|
||
return $this->db()->table('fin_accounts')
|
||
->select('account_short')->distinct()->orderBy('account_short')->pluck('account_short')->all();
|
||
}
|
||
|
||
private function manualTransactionAccountOptions(): array
|
||
{
|
||
return $this->db()->table('fin_accounts')
|
||
->select(['account_short', 'company_name'])
|
||
->whereNotNull('account_short')
|
||
->where('account_short', '<>', '')
|
||
->orderBy('account_short')
|
||
->orderBy('company_name')
|
||
->orderBy('id')
|
||
->get()
|
||
->groupBy('account_short')
|
||
->map(static function ($rows, string $accountShort): array {
|
||
$first = $rows->first();
|
||
$companyName = trim((string) ($first->company_name ?? ''));
|
||
$accountCount = $rows->count();
|
||
|
||
return [
|
||
'value' => $accountShort,
|
||
'label' => $accountShort
|
||
. ($companyName !== '' ? ' · ' . $companyName : '')
|
||
. ($accountCount > 1 ? '(' . $accountCount . ' 个账号)' : ''),
|
||
];
|
||
})
|
||
->values()
|
||
->all();
|
||
}
|
||
|
||
private function accountFilterOptions(): array
|
||
{
|
||
$baseTypes = $this->db()->table('fin_accounts')
|
||
->whereNotNull('account_type')->where('account_type', '<>', '')
|
||
->distinct()->orderBy('account_type')->pluck('account_type')->all();
|
||
$overrideTypes = $this->db()->table('fin_account_type_overrides')
|
||
->whereNotNull('account_type')->where('account_type', '<>', '')
|
||
->distinct()->orderBy('account_type')->pluck('account_type')->all();
|
||
$types = FinanceLedgerAccountType::options($baseTypes, $overrideTypes);
|
||
|
||
return [
|
||
'regions' => $this->db()->table('fin_accounts')
|
||
->whereNotNull('region')->where('region', '<>', '')
|
||
->distinct()->orderBy('region')->pluck('region')->all(),
|
||
'types' => $types,
|
||
];
|
||
}
|
||
|
||
private function accountPlanOptions(): array
|
||
{
|
||
return $this->db()->table('fin_accounts')
|
||
->whereNotNull('company_plan')
|
||
->where('company_plan', '<>', '')
|
||
->distinct()
|
||
->orderBy('company_plan')
|
||
->pluck('company_plan')
|
||
->map(static fn (mixed $value): string => trim((string) $value))
|
||
->filter()
|
||
->unique()
|
||
->values()
|
||
->all();
|
||
}
|
||
|
||
private function companyOptions(): array
|
||
{
|
||
return $this->db()->table('fin_companies')
|
||
->whereNotNull('name')
|
||
->pluck('name')
|
||
->merge(
|
||
$this->db()->table('fin_accounts')
|
||
->whereNotNull('company_name')
|
||
->distinct()
|
||
->pluck('company_name'),
|
||
)
|
||
->map(static fn (mixed $value): string => trim((string) $value))
|
||
->filter()
|
||
->unique()
|
||
->sortBy(static fn (string $value): string => mb_strtolower($value))
|
||
->values()
|
||
->all();
|
||
}
|
||
|
||
private function applyLedgerHeaderFilter(Builder $query, string $tab, array $columns): void
|
||
{
|
||
$column = trim((string) request($tab . '_filter_column', ''));
|
||
$value = trim((string) request($tab . '_filter_value', ''));
|
||
if ($value === '' || ! isset($columns[$column])) {
|
||
return;
|
||
}
|
||
|
||
$definition = $columns[$column];
|
||
$expression = $definition['filter'] ?? $definition['sort'] ?? null;
|
||
if (! is_string($expression) || $expression === '') {
|
||
return;
|
||
}
|
||
|
||
$type = $definition['type'] ?? 'text';
|
||
if ($type === 'number') {
|
||
if (is_numeric($value)) {
|
||
$query->whereRaw($expression . ' = ?', [$value]);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (in_array($type, ['enum', 'date'], true)) {
|
||
$query->whereRaw($expression . ' = ?', [$value]);
|
||
|
||
return;
|
||
}
|
||
|
||
$query->whereRaw($expression . ' LIKE ?', ['%' . $value . '%']);
|
||
}
|
||
|
||
private function applyLedgerHeaderSort(Builder $query, string $tab, array $columns): bool
|
||
{
|
||
$column = trim((string) request($tab . '_sort', ''));
|
||
if (! isset($columns[$column])) {
|
||
return false;
|
||
}
|
||
|
||
$expression = $columns[$column]['sort'] ?? null;
|
||
if (! is_string($expression) || $expression === '') {
|
||
return false;
|
||
}
|
||
|
||
$direction = strtolower((string) request($tab . '_direction', 'asc')) === 'desc' ? 'desc' : 'asc';
|
||
$query->orderByRaw($expression . ' ' . $direction);
|
||
|
||
return true;
|
||
}
|
||
|
||
private function filterAndSortHeaderRows(array $rows, string $tab, array $columns): array
|
||
{
|
||
$filterColumn = trim((string) request($tab . '_filter_column', ''));
|
||
$filterValue = trim((string) request($tab . '_filter_value', ''));
|
||
if ($filterValue !== '' && isset($columns[$filterColumn])) {
|
||
$valueColumn = $columns[$filterColumn]['value'] ?? $filterColumn;
|
||
$type = $columns[$filterColumn]['type'] ?? 'text';
|
||
$rows = array_values(array_filter(
|
||
$rows,
|
||
static function (array $row) use ($valueColumn, $filterValue, $type): bool {
|
||
$value = trim((string) ($row[$valueColumn] ?? ''));
|
||
if ($type === 'number') {
|
||
return is_numeric($filterValue) && is_numeric($value) && (float) $value === (float) $filterValue;
|
||
}
|
||
if (in_array($type, ['enum', 'date'], true)) {
|
||
return $value === $filterValue;
|
||
}
|
||
|
||
return mb_stripos($value, $filterValue) !== false;
|
||
},
|
||
));
|
||
}
|
||
|
||
$sortColumn = trim((string) request($tab . '_sort', ''));
|
||
if (isset($columns[$sortColumn])) {
|
||
$valueColumn = $columns[$sortColumn]['value'] ?? $sortColumn;
|
||
$direction = strtolower((string) request($tab . '_direction', 'asc')) === 'desc' ? -1 : 1;
|
||
$type = $columns[$sortColumn]['type'] ?? 'text';
|
||
usort($rows, static function (array $left, array $right) use ($valueColumn, $direction, $type): int {
|
||
$leftValue = $left[$valueColumn] ?? null;
|
||
$rightValue = $right[$valueColumn] ?? null;
|
||
if ($type === 'number') {
|
||
return ((float) $leftValue <=> (float) $rightValue) * $direction;
|
||
}
|
||
|
||
return strnatcasecmp((string) $leftValue, (string) $rightValue) * $direction;
|
||
});
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
private function accountHeaderColumns(): array
|
||
{
|
||
$a = $this->queryAlias('a');
|
||
$statusTable = $this->tableName('fin_account_statuses');
|
||
$typeTable = $this->tableName('fin_account_type_overrides');
|
||
$statusExpression = "COALESCE((SELECT NULLIF(s.status, '') FROM {$statusTable} s WHERE s.account_short = {$a}.account_short LIMIT 1), 'normal')";
|
||
$typeExpression = "COALESCE((SELECT NULLIF(o.account_type, '') FROM {$typeTable} o WHERE o.account_short = {$a}.account_short LIMIT 1), NULLIF({$a}.account_type, ''), '')";
|
||
$keepExpression = "COALESCE(NULLIF({$a}.account_should_keep, ''), CASE WHEN COALESCE({$a}.company_plan, '') = '' THEN '' WHEN {$a}.company_plan LIKE '%保留%' THEN '是' ELSE '否' END)";
|
||
|
||
return [
|
||
'company_name' => ['sort' => $a . '.company_name', 'type' => 'text'],
|
||
'account_short' => ['sort' => $a . '.account_short', 'type' => 'text'],
|
||
'account_type' => ['sort' => $typeExpression, 'type' => 'enum'],
|
||
'account_status' => ['sort' => $statusExpression, 'type' => 'enum'],
|
||
'region' => ['sort' => $a . '.region', 'type' => 'text'],
|
||
'account_no' => ['sort' => $a . '.account_no', 'type' => 'text'],
|
||
'bank_name' => ['sort' => $a . '.bank_name', 'type' => 'text'],
|
||
'bank_city_branch' => ['sort' => $a . '.bank_city_branch', 'type' => 'text'],
|
||
'company_plan' => ['sort' => $a . '.company_plan', 'type' => 'enum'],
|
||
'account_should_keep' => ['sort' => $keepExpression, 'type' => 'enum'],
|
||
'account_last4' => ['sort' => $a . '.account_last4', 'type' => 'text'],
|
||
'single_limit' => ['sort' => "CAST({$a}.single_limit AS DECIMAL(18, 2))", 'filter' => $a . '.single_limit', 'type' => 'number'],
|
||
'daily_limit' => ['sort' => "CAST({$a}.daily_limit AS DECIMAL(18, 2))", 'filter' => $a . '.daily_limit', 'type' => 'number'],
|
||
'monthly_limit' => ['sort' => "CAST({$a}.monthly_limit AS DECIMAL(18, 2))", 'filter' => $a . '.monthly_limit', 'type' => 'number'],
|
||
'yearly_limit' => ['sort' => "CAST({$a}.yearly_limit AS DECIMAL(18, 2))", 'filter' => $a . '.yearly_limit', 'type' => 'number'],
|
||
'current_balance' => ['sort' => $a . '.current_balance', 'type' => 'number'],
|
||
'daily_available' => ['sort' => $a . '.daily_available', 'type' => 'number'],
|
||
'blocked_amount' => ['sort' => $a . '.blocked_amount', 'type' => 'number'],
|
||
'daily_withdraw' => ['sort' => $a . '.daily_available', 'type' => 'number'],
|
||
'clearing_days' => ['sort' => $a . '.clearing_days', 'type' => 'number'],
|
||
'raise_target' => ['sort' => "CAST({$a}.raise_target AS DECIMAL(18, 2))", 'filter' => $a . '.raise_target', 'type' => 'number'],
|
||
'raise_status' => ['sort' => $a . '.raise_status', 'type' => 'text'],
|
||
'raise_actions' => ['sort' => $a . '.raise_actions', 'type' => 'text'],
|
||
'done_actions' => ['sort' => $a . '.done_actions', 'type' => 'text'],
|
||
'next_action' => ['sort' => $a . '.next_action', 'type' => 'text'],
|
||
'remark' => ['sort' => $a . '.remark', 'type' => 'text'],
|
||
'owner' => ['sort' => $a . '.owner', 'type' => 'text'],
|
||
];
|
||
}
|
||
|
||
private function shopHeaderColumns(): array
|
||
{
|
||
return [
|
||
'platform' => ['sort' => 'platform', 'type' => 'text'],
|
||
'shop_name' => ['sort' => 'shop_name', 'type' => 'text'],
|
||
'company_name' => ['sort' => 'company_name', 'type' => 'text'],
|
||
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
|
||
'status' => ['sort' => 'status', 'type' => 'text'],
|
||
'store_manager' => ['sort' => 'shop_name', 'type' => 'text'],
|
||
'department_manager' => ['sort' => 'shop_name', 'type' => 'text'],
|
||
'payout_account' => ['sort' => 'payout_account', 'type' => 'text'],
|
||
'shop_balance_source' => ['sort' => 'shop_balance_source', 'type' => 'text'],
|
||
'shop_balance' => ['sort' => 'shop_balance', 'type' => 'number'],
|
||
'reserve_standard' => ['sort' => 'reserve_standard', 'type' => 'number'],
|
||
'balance_alert' => ['sort' => 'balance_alert', 'type' => 'enum'],
|
||
'remark' => ['sort' => 'remark', 'type' => 'text'],
|
||
];
|
||
}
|
||
|
||
private function flowHeaderColumns(): array
|
||
{
|
||
return [
|
||
'flow_date' => ['sort' => 'flow_date', 'type' => 'date'],
|
||
'company_name' => ['sort' => 'company_name', 'type' => 'text'],
|
||
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
|
||
'opening_balance' => ['sort' => 'opening_balance', 'type' => 'number'],
|
||
'inflow' => ['sort' => 'inflow', 'type' => 'number'],
|
||
'outflow' => ['sort' => 'outflow', 'type' => 'number'],
|
||
'closing_balance' => ['sort' => 'closing_balance', 'type' => 'number'],
|
||
'remark' => ['sort' => 'remark', 'type' => 'text'],
|
||
];
|
||
}
|
||
|
||
private function transactionHeaderColumns(): array
|
||
{
|
||
$t = $this->queryAlias('t');
|
||
$m = $this->queryAlias('m');
|
||
$effectiveAccount = "COALESCE(NULLIF({$m}.account_short, ''), NULLIF({$t}.account_short, ''), '未匹配')";
|
||
|
||
return [
|
||
'txn_date' => ['sort' => $t . '.txn_date', 'type' => 'date'],
|
||
'effective_account_short' => ['sort' => $effectiveAccount, 'type' => 'text'],
|
||
'detail' => ['sort' => $t . '.detail', 'type' => 'text'],
|
||
'source' => ['sort' => $t . '.source', 'type' => 'enum'],
|
||
'income' => ['sort' => $t . '.income', 'type' => 'number'],
|
||
'expense' => ['sort' => $t . '.expense', 'type' => 'number'],
|
||
'approval_no' => ['sort' => $t . '.approval_no', 'type' => 'text'],
|
||
];
|
||
}
|
||
|
||
private function mappingHeaderColumns(): array
|
||
{
|
||
$mappingStatus = "CASE WHEN COALESCE(account_short, '') = '' THEN 'unmapped' ELSE 'mapped' END";
|
||
|
||
return [
|
||
'source' => ['sort' => 'source', 'type' => 'enum'],
|
||
'source_value' => ['sort' => 'source_value', 'type' => 'text'],
|
||
'txn_count' => ['sort' => 'txn_count', 'type' => 'number'],
|
||
'income_sum' => ['sort' => 'income_sum', 'type' => 'number'],
|
||
'expense_sum' => ['sort' => 'expense_sum', 'type' => 'number'],
|
||
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
|
||
'mapping_status' => ['sort' => $mappingStatus, 'type' => 'enum'],
|
||
];
|
||
}
|
||
|
||
private function sourceHeaderColumns(): array
|
||
{
|
||
return [
|
||
'source' => ['value' => 'source', 'type' => 'enum'],
|
||
'process_type' => ['value' => 'process_type', 'type' => 'text'],
|
||
'rows_count' => ['value' => 'rows_count', 'type' => 'number'],
|
||
'income' => ['value' => 'income', 'type' => 'number'],
|
||
'expense' => ['value' => 'expense', 'type' => 'number'],
|
||
'date_range' => ['value' => 'date_range', 'type' => 'text'],
|
||
];
|
||
}
|
||
|
||
private function db()
|
||
{
|
||
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||
}
|
||
|
||
private function tableName(string $table): string
|
||
{
|
||
return $this->db()->getTablePrefix() . $table;
|
||
}
|
||
|
||
private function queryAlias(string $alias): string
|
||
{
|
||
return $this->db()->getTablePrefix() . $alias;
|
||
}
|
||
}
|