370 lines
13 KiB
PHP
370 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Repositories;
|
|
|
|
use Illuminate\Database\ConnectionInterface;
|
|
use Illuminate\Database\DatabaseManager;
|
|
use Illuminate\Database\Query\Builder;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use RuntimeException;
|
|
|
|
class DingProcessDetailMirrorRepository
|
|
{
|
|
public const DEFAULT_SCOPE = 'personal_permission';
|
|
|
|
public const DATASET_PREFIX = 'ding.process_details.';
|
|
|
|
public const PAYMENT_STATES = [
|
|
'出纳状态待同步',
|
|
'出纳未开始',
|
|
'出纳待打款',
|
|
'出纳退回',
|
|
'出纳已打款',
|
|
'流程终止',
|
|
'不涉及付款',
|
|
];
|
|
|
|
public const LEGACY_PAYMENT_STATE = '未确认';
|
|
|
|
private readonly ConnectionInterface $connection;
|
|
|
|
private ?bool $paymentSourceStateColumn = null;
|
|
|
|
private ?bool $cashierStatusColumn = null;
|
|
|
|
public function __construct(DatabaseManager $database)
|
|
{
|
|
$this->connection = $database->connection(
|
|
(string) config('upstream_mirrors.connection', 'business_mirror')
|
|
);
|
|
}
|
|
|
|
public function scope(): string
|
|
{
|
|
$scope = trim((string) config(
|
|
'upstream_mirrors.ding_process_details.scope',
|
|
self::DEFAULT_SCOPE,
|
|
));
|
|
|
|
return $scope !== '' ? $scope : self::DEFAULT_SCOPE;
|
|
}
|
|
|
|
public function datasetKey(): string
|
|
{
|
|
return self::DATASET_PREFIX . $this->scope();
|
|
}
|
|
|
|
public function ready(): bool
|
|
{
|
|
$schema = Schema::connection($this->connection->getName());
|
|
|
|
if (! $schema->hasTable('ding_process_detail_mirror')
|
|
|| ! $schema->hasTable('business_mirror_table_states')) {
|
|
return false;
|
|
}
|
|
|
|
$state = $this->state();
|
|
|
|
return $state !== null && ($state['initial_completed_at'] ?? null) !== null;
|
|
}
|
|
|
|
/** @return array<string,mixed>|null */
|
|
public function state(): ?array
|
|
{
|
|
if (! Schema::connection($this->connection->getName())->hasTable('business_mirror_table_states')) {
|
|
return null;
|
|
}
|
|
|
|
$row = $this->connection->table('business_mirror_table_states')
|
|
->where('dataset_key', $this->datasetKey())
|
|
->first();
|
|
|
|
return $row === null ? null : (array) $row;
|
|
}
|
|
|
|
/**
|
|
* @param array<string,string> $filters
|
|
* @return array{
|
|
* data:list<object>,
|
|
* total:int,
|
|
* summary:array{rows:int,approvals:int,amount_total:float,bank_amount_total:float},
|
|
* status:string,
|
|
* last_synced_at:?string,
|
|
* sync_stale:bool,
|
|
* sync_stale_after_minutes:int,
|
|
* scope:string
|
|
* }
|
|
*/
|
|
public function paginate(array $filters, int $page, int $perPage): array
|
|
{
|
|
if (! $this->ready()) {
|
|
throw new RuntimeException('ding_process_detail_mirror_not_ready');
|
|
}
|
|
|
|
$page = max($page, 1);
|
|
$perPage = min(max($perPage, 1), 100);
|
|
$query = $this->filteredQuery($filters);
|
|
|
|
$total = (clone $query)->count();
|
|
$bankAmountTotal = $this->structuredPaymentsEnabled()
|
|
? (float) ($this->connection->table('ding_process_payment_mirror')
|
|
->where('source_scope', $this->scope())
|
|
->whereIn(
|
|
'business_id',
|
|
(clone $query)->select('ding_process_detail_mirror.business_id'),
|
|
)
|
|
->sum('amount') ?? 0)
|
|
: (float) ((clone $query)->sum('ding_process_detail_mirror.bank_amount') ?? 0);
|
|
$summary = [
|
|
'rows' => $total,
|
|
'approvals' => (clone $query)->distinct()->count('ding_process_detail_mirror.business_id'),
|
|
'amount_total' => (float) ((clone $query)->sum('ding_process_detail_mirror.amount') ?? 0),
|
|
'bank_amount_total' => $bankAmountTotal,
|
|
];
|
|
$data = $query
|
|
->orderByDesc('ding_process_detail_mirror.business_month')
|
|
->orderByDesc('ding_process_detail_mirror.business_id')
|
|
->orderBy('ding_process_detail_mirror.line_no')
|
|
->forPage($page, $perPage)
|
|
->get()
|
|
->all();
|
|
$state = $this->state();
|
|
$staleAfterMinutes = max(
|
|
15,
|
|
(int) config('upstream_mirrors.ding_process_details.sync_interval_minutes', 5) * 3,
|
|
);
|
|
|
|
return [
|
|
'data' => $data,
|
|
'total' => $total,
|
|
'summary' => $summary,
|
|
'status' => (string) ($state['status'] ?? 'unknown'),
|
|
'last_synced_at' => isset($state['last_succeeded_at'])
|
|
? (string) $state['last_succeeded_at']
|
|
: null,
|
|
'sync_stale' => $this->stateIsStale($state, $staleAfterMinutes),
|
|
'sync_stale_after_minutes' => $staleAfterMinutes,
|
|
'scope' => $this->scope(),
|
|
];
|
|
}
|
|
|
|
/** @return array{document_types:list<string>,statuses:list<string>,payment_states:list<string>} */
|
|
public function filterOptions(): array
|
|
{
|
|
if (! Schema::connection($this->connection->getName())->hasTable('ding_process_detail_mirror')) {
|
|
return ['document_types' => [], 'statuses' => [], 'payment_states' => []];
|
|
}
|
|
|
|
$base = $this->connection->table('ding_process_detail_mirror')
|
|
->where('source_scope', $this->scope());
|
|
|
|
return [
|
|
'document_types' => (clone $base)
|
|
->whereNotNull('document_type')
|
|
->where('document_type', '!=', '')
|
|
->distinct()
|
|
->orderBy('document_type')
|
|
->pluck('document_type')
|
|
->map(static fn (mixed $value): string => (string) $value)
|
|
->all(),
|
|
'statuses' => (clone $base)
|
|
->whereNotNull('status')
|
|
->where('status', '!=', '')
|
|
->distinct()
|
|
->orderBy('status')
|
|
->pluck('status')
|
|
->map(static fn (mixed $value): string => (string) $value)
|
|
->all(),
|
|
'payment_states' => $this->structuredPaymentsEnabled()
|
|
? self::PAYMENT_STATES
|
|
: [self::LEGACY_PAYMENT_STATE],
|
|
];
|
|
}
|
|
|
|
/** @param array<string,string> $filters */
|
|
public function exportCount(array $filters): int
|
|
{
|
|
if (! Schema::connection($this->connection->getName())->hasTable('ding_process_detail_mirror')) {
|
|
return 0;
|
|
}
|
|
|
|
return (int) $this->filteredQuery($filters)->count();
|
|
}
|
|
|
|
/**
|
|
* @param array<string,string> $filters
|
|
* @return \Generator<int,array<string,mixed>>
|
|
*/
|
|
public function exportRows(array $filters): \Generator
|
|
{
|
|
$query = $this->filteredQuery($filters)
|
|
->orderByDesc('business_month')
|
|
->orderByDesc('business_id')
|
|
->orderBy('line_no');
|
|
|
|
foreach ($query->cursor() as $row) {
|
|
yield (array) $row;
|
|
}
|
|
}
|
|
|
|
/** @param array<string,string> $filters */
|
|
private function filteredQuery(array $filters): Builder
|
|
{
|
|
$query = $this->connection->table('ding_process_detail_mirror')
|
|
->select('ding_process_detail_mirror.*');
|
|
|
|
if ($this->structuredPaymentsEnabled()) {
|
|
$detailTotals = $this->connection->table('ding_process_detail_mirror')
|
|
->select('business_id')
|
|
->selectRaw('SUM(COALESCE(amount, 0)) as approval_amount')
|
|
->where('source_scope', $this->scope())
|
|
->groupBy('business_id');
|
|
$paymentTotals = $this->connection->table('ding_process_payment_mirror')
|
|
->select('business_id')
|
|
->selectRaw('SUM(COALESCE(amount, 0)) as payment_amount')
|
|
->where('source_scope', $this->scope())
|
|
->groupBy('business_id');
|
|
|
|
$query
|
|
->leftJoinSub($detailTotals, 'detail_totals', function ($join): void {
|
|
$join->on(
|
|
'detail_totals.business_id',
|
|
'=',
|
|
'ding_process_detail_mirror.business_id',
|
|
);
|
|
})
|
|
->leftJoinSub($paymentTotals, 'payment_totals', function ($join): void {
|
|
$join->on(
|
|
'payment_totals.business_id',
|
|
'=',
|
|
'ding_process_detail_mirror.business_id',
|
|
);
|
|
})
|
|
->selectRaw(
|
|
'COALESCE(payment_totals.payment_amount, 0) as payment_fact_amount, '
|
|
. 'COALESCE(detail_totals.approval_amount, 0) '
|
|
. '- COALESCE(payment_totals.payment_amount, 0) as payment_difference, '
|
|
. $this->paymentStateExpression() . ' as payment_state',
|
|
);
|
|
} else {
|
|
$query
|
|
->selectRaw(
|
|
'NULL as payment_fact_amount, NULL as payment_difference, ? as payment_state',
|
|
[self::LEGACY_PAYMENT_STATE],
|
|
);
|
|
}
|
|
|
|
return $this->applyFilters($query, $filters);
|
|
}
|
|
|
|
/** @param array<string,string> $filters */
|
|
private function applyFilters(Builder $query, array $filters): Builder
|
|
{
|
|
$query->where('ding_process_detail_mirror.source_scope', $this->scope());
|
|
|
|
$paidDate = trim((string) ($filters['paid_date'] ?? ''));
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $paidDate) === 1) {
|
|
$query->where('ding_process_detail_mirror.paid_at', 'like', $paidDate . '%');
|
|
}
|
|
|
|
foreach ([
|
|
'document_type' => 'document_type',
|
|
'status' => 'status',
|
|
] as $filter => $column) {
|
|
$value = trim((string) ($filters[$filter] ?? ''));
|
|
if ($value !== '') {
|
|
$query->where('ding_process_detail_mirror.' . $column, $value);
|
|
}
|
|
}
|
|
|
|
foreach ([
|
|
'business_id' => 'business_id',
|
|
'originator_name' => 'originator_name',
|
|
'keyword' => 'purpose',
|
|
] as $filter => $column) {
|
|
$value = mb_substr(trim((string) ($filters[$filter] ?? '')), 0, 100);
|
|
if ($value !== '') {
|
|
$query->where('ding_process_detail_mirror.' . $column, 'like', '%' . $value . '%');
|
|
}
|
|
}
|
|
|
|
$paymentState = trim((string) ($filters['payment_state'] ?? ''));
|
|
if ($paymentState !== '') {
|
|
$allowed = $this->structuredPaymentsEnabled()
|
|
? self::PAYMENT_STATES
|
|
: [self::LEGACY_PAYMENT_STATE];
|
|
if (in_array($paymentState, $allowed, true)) {
|
|
if ($paymentState !== self::LEGACY_PAYMENT_STATE) {
|
|
$query->whereRaw(
|
|
'(' . $this->paymentStateExpression() . ') = ?',
|
|
[$paymentState],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
private function paymentStateExpression(): string
|
|
{
|
|
$sourceState = $this->paymentSourceStateColumnAvailable()
|
|
? 'ding_process_detail_mirror.payment_source_state'
|
|
: "'missing'";
|
|
$cashierState = $this->cashierStatusColumnAvailable()
|
|
? 'ding_process_detail_mirror.cashier_status'
|
|
: "'pending_sync'";
|
|
|
|
return "CASE
|
|
WHEN {$sourceState} = 'not_applicable' THEN '不涉及付款'
|
|
WHEN {$cashierState} = 'completed' THEN '出纳已打款'
|
|
WHEN {$cashierState} = 'pending' THEN '出纳待打款'
|
|
WHEN {$cashierState} = 'returned' THEN '出纳退回'
|
|
WHEN {$cashierState} = 'not_started' THEN '出纳未开始'
|
|
WHEN {$cashierState} = 'terminated' THEN '流程终止'
|
|
ELSE '出纳状态待同步'
|
|
END";
|
|
}
|
|
|
|
private function paymentSourceStateColumnAvailable(): bool
|
|
{
|
|
return $this->paymentSourceStateColumn ??= Schema::connection(
|
|
$this->connection->getName(),
|
|
)->hasColumn('ding_process_detail_mirror', 'payment_source_state');
|
|
}
|
|
|
|
private function cashierStatusColumnAvailable(): bool
|
|
{
|
|
return $this->cashierStatusColumn ??= Schema::connection(
|
|
$this->connection->getName(),
|
|
)->hasColumn('ding_process_detail_mirror', 'cashier_status');
|
|
}
|
|
|
|
/** @param array<string,mixed>|null $state */
|
|
private function stateIsStale(?array $state, int $staleAfterMinutes): bool
|
|
{
|
|
if (($state['status'] ?? '') !== 'succeeded' || empty($state['last_succeeded_at'])) {
|
|
return true;
|
|
}
|
|
|
|
return Carbon::parse((string) $state['last_succeeded_at'])
|
|
->lt(now()->subMinutes($staleAfterMinutes));
|
|
}
|
|
|
|
private function structuredPaymentsEnabled(): bool
|
|
{
|
|
if (! Schema::connection($this->connection->getName())->hasTable('ding_process_payment_mirror')) {
|
|
return false;
|
|
}
|
|
|
|
$state = $this->state();
|
|
$cursor = $state['cursor'] ?? null;
|
|
if (is_string($cursor)) {
|
|
$cursor = json_decode($cursor, true);
|
|
}
|
|
|
|
return is_array($cursor) && ($cursor['source'] ?? '') === 'structured';
|
|
}
|
|
}
|