171 lines
5.5 KiB
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;
|
|
}
|
|
}
|