CaiWuXiTong/modules/EcommerceFinance/Services/DingProcessPaymentLineAllocator.php

171 lines
5.5 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Support\Carbon;
final class DingProcessPaymentLineAllocator
{
/**
* Allocate every structured payment to its referenced OA detail lines.
*
* @param list<array<string,mixed>> $details
* @param list<array<string,mixed>> $payments
* @return list<array<string,mixed>>
*/
public function allocate(
array $details,
array $payments,
string $scope,
Carbon $now,
): array {
$detailsByBusiness = [];
foreach ($details as $detail) {
$businessId = trim((string) ($detail['business_id'] ?? ''));
$lineNo = (int) ($detail['line_no'] ?? 0);
if ($businessId === '' || $lineNo <= 0) {
continue;
}
$detailsByBusiness[$businessId][$lineNo] = [
'amount_cents' => $this->amountCents($detail),
];
}
$rows = [];
foreach ($payments as $payment) {
$businessId = trim((string) ($payment['business_id'] ?? ''));
$paymentNo = (int) ($payment['payment_no'] ?? 0);
$amountCents = $this->amountCents($payment);
$businessDetails = $detailsByBusiness[$businessId] ?? [];
if ($businessId === '' || $paymentNo <= 0 || $amountCents === 0 || $businessDetails === []) {
continue;
}
$lineNumbers = $this->lineNumbers(
(string) ($payment['sequence_spec'] ?? ''),
$businessDetails,
$amountCents,
);
if ($lineNumbers === []) {
continue;
}
$allocated = $this->allocateCents(
$amountCents,
array_map(
static fn (int $lineNo): int => (int) $businessDetails[$lineNo]['amount_cents'],
$lineNumbers,
),
);
$paidAt = trim((string) ($payment['paid_at'] ?? ''));
$paidMonth = preg_match('/^\d{4}-\d{2}/', $paidAt, $matches) === 1
? $matches[0]
: '';
foreach ($lineNumbers as $index => $lineNo) {
$rows[] = [
'source_scope' => $scope,
'business_id' => $businessId,
'process_instance_id' => (string) ($payment['process_instance_id'] ?? ''),
'payment_no' => $paymentNo,
'line_no' => $lineNo,
'paid_month' => $paidMonth,
'paid_at' => $paidAt,
'amount' => number_format($allocated[$index] / 100, 2, '.', ''),
'account_code' => $payment['account_code'] ?? null,
'account_name' => $payment['account_name'] ?? null,
'payment_method' => $payment['payment_method'] ?? null,
'synced_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
}
}
return $rows;
}
/** @param array<string,mixed> $row */
private function amountCents(array $row): int
{
if (array_key_exists('amount_cents', $row)) {
return (int) $row['amount_cents'];
}
return (int) round((float) ($row['amount'] ?? 0) * 100);
}
/**
* @param array<int,array{amount_cents:int}> $businessDetails
* @return list<int>
*/
private function lineNumbers(
string $sequenceSpec,
array $businessDetails,
int $amountCents,
): array
{
$availableLines = array_map('intval', array_keys($businessDetails));
sort($availableLines);
if ($availableLines === []) {
return [];
}
preg_match_all('/\d+/', $sequenceSpec, $matches);
$requested = array_values(array_unique(array_map('intval', $matches[0] ?? [])));
$matched = array_values(array_intersect($requested, $availableLines));
if ($matched !== []) {
return $matched;
}
$exactLines = [];
foreach ($businessDetails as $lineNo => $detail) {
if ((int) $detail['amount_cents'] === $amountCents) {
$exactLines[] = (int) $lineNo;
}
}
if (count($exactLines) === 1) {
return $exactLines;
}
// 序号为空或引用了不存在的明细行时,仍按全部明细金额分摊,
// 保证每一笔已付款都能进入对应审批,不因单行数据错误整笔漏计。
return $availableLines;
}
/**
* @param list<int> $weights
* @return list<int>
*/
private function allocateCents(int $amountCents, array $weights): array
{
$count = count($weights);
if ($count === 0) {
return [];
}
$totalWeight = array_sum($weights);
if ($totalWeight <= 0) {
$base = intdiv($amountCents, $count);
$allocated = array_fill(0, $count, $base);
$allocated[$count - 1] += $amountCents - ($base * $count);
return $allocated;
}
$allocated = [];
$used = 0;
foreach ($weights as $index => $weight) {
if ($index === $count - 1) {
$value = $amountCents - $used;
} else {
$value = intdiv($amountCents * $weight, $totalWeight);
$used += $value;
}
$allocated[] = $value;
}
return $allocated;
}
}