where('id', $originalBillId)->first(); if ($original === null) { throw new \InvalidArgumentException('bill_not_found'); } if ($this->periodCompletion->isPeriodReadOnly((int) $original->settlement_period_id)) { throw ValidationException::withMessages([ 'period' => ['completed'], ]); } if (! in_array((string) $original->status, ['confirmed', 'partial_paid', 'overdue'], 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; $archiveBillId = (int) DB::table('settlement_bills')->insertGetId([ 'settlement_period_id' => $periodId, 'bill_type' => 'bad_debt', 'owner_type' => (string) $original->owner_type, 'owner_id' => (int) $original->owner_id, 'counterparty_type' => (string) $original->counterparty_type, 'counterparty_id' => (int) $original->counterparty_id, 'gross_win_loss' => 0, 'rebate_amount' => 0, 'adjustment_amount' => -$unpaid, 'platform_rounding_adjustment' => 0, 'net_amount' => 0, 'paid_amount' => 0, 'unpaid_amount' => 0, 'status' => 'settled', 'reversed_bill_id' => $originalBillId, 'meta_json' => json_encode([ 'original_bill_id' => $originalBillId, 'written_off_amount' => $unpaid, 'original_net_amount' => (int) $original->net_amount, ]), 'locked_at' => $now, 'confirmed_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]); DB::table('settlement_adjustments')->insert([ 'settlement_period_id' => $periodId, 'original_bill_id' => $originalBillId, 'adjustment_type' => 'bad_debt', 'amount' => $unpaid, 'reason' => $reason, 'created_by' => $adminUserId > 0 ? $adminUserId : null, 'created_at' => $now, 'updated_at' => $now, ]); DB::table('settlement_bills')->where('id', $originalBillId)->update([ 'unpaid_amount' => 0, 'status' => 'settled', 'meta_json' => json_encode(array_merge( $this->decodeMeta($original->meta_json), [ 'bad_debt_bill_id' => $archiveBillId, 'written_off_amount' => $unpaid, ], )), 'updated_at' => $now, ]); $this->periodCompletion->syncIfReady($periodId); return $archiveBillId; }); } /** * @return array */ private function decodeMeta(mixed $metaJson): array { if ($metaJson === null || $metaJson === '') { return []; } if (is_array($metaJson)) { return $metaJson; } $decoded = json_decode((string) $metaJson, true); return is_array($decoded) ? $decoded : []; } }