fix(funds): 加固转账冲正、结算收付幂等与坏账核销
- 转入 main_site_timeout 等不可冲正场景直接拒绝,避免假结案 - payment_records / settlement_adjustments 增加 partial unique 索引 - 坏账核销行锁 + meta 幂等回放;补差单记录 result_bill_id - 新增 FundOperationsHardeningTest 覆盖关键路径
This commit is contained in:
@@ -17,37 +17,44 @@ final class AgentSettlementBadDebtService
|
||||
|
||||
public function writeOff(int $originalBillId, ?string $reason, int $adminUserId): int
|
||||
{
|
||||
$original = DB::table('settlement_bills')->where('id', $originalBillId)->first();
|
||||
if ($original === null) {
|
||||
throw new \InvalidArgumentException('bill_not_found');
|
||||
}
|
||||
return (int) DB::transaction(function () use ($originalBillId, $reason, $adminUserId): int {
|
||||
/** @var object|null $original */
|
||||
$original = DB::table('settlement_bills')->where('id', $originalBillId)->lockForUpdate()->first();
|
||||
if ($original === null) {
|
||||
throw new \InvalidArgumentException('bill_not_found');
|
||||
}
|
||||
|
||||
if ($this->periodCompletion->isPeriodReadOnly((int) $original->settlement_period_id)) {
|
||||
throw ValidationException::withMessages([
|
||||
'period' => ['completed'],
|
||||
]);
|
||||
}
|
||||
$meta = $this->decodeMeta($original->meta_json);
|
||||
$existingArchiveId = (int) ($meta['bad_debt_bill_id'] ?? 0);
|
||||
if ($existingArchiveId > 0) {
|
||||
return $existingArchiveId;
|
||||
}
|
||||
|
||||
if (! in_array((string) $original->status, ['confirmed', 'partial_paid', 'overdue'], true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'bill' => ['not_eligible'],
|
||||
]);
|
||||
}
|
||||
if ($this->periodCompletion->isPeriodReadOnly((int) $original->settlement_period_id)) {
|
||||
throw ValidationException::withMessages([
|
||||
'period' => ['completed'],
|
||||
]);
|
||||
}
|
||||
|
||||
$unpaid = (int) $original->unpaid_amount;
|
||||
if ($unpaid <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'bill' => ['no_unpaid'],
|
||||
]);
|
||||
}
|
||||
if (! in_array((string) $original->status, ['confirmed', 'partial_paid', 'overdue'], true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'bill' => ['not_eligible'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (in_array((string) $original->bill_type, ['adjustment', 'reversal', 'bad_debt'], true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'bill' => ['not_eligible'],
|
||||
]);
|
||||
}
|
||||
$unpaid = (int) $original->unpaid_amount;
|
||||
if ($unpaid <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'bill' => ['no_unpaid'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (in_array((string) $original->bill_type, ['adjustment', 'reversal', 'bad_debt'], true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'bill' => ['not_eligible'],
|
||||
]);
|
||||
}
|
||||
|
||||
return (int) DB::transaction(function () use ($original, $originalBillId, $unpaid, $reason, $adminUserId): int {
|
||||
$now = now();
|
||||
$periodId = (int) $original->settlement_period_id;
|
||||
|
||||
@@ -93,7 +100,7 @@ final class AgentSettlementBadDebtService
|
||||
'unpaid_amount' => 0,
|
||||
'status' => 'settled',
|
||||
'meta_json' => json_encode(array_merge(
|
||||
$this->decodeMeta($original->meta_json),
|
||||
$meta,
|
||||
[
|
||||
'bad_debt_bill_id' => $archiveBillId,
|
||||
'written_off_amount' => $unpaid,
|
||||
@@ -133,4 +140,4 @@ final class AgentSettlementBadDebtService
|
||||
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user