Fix 3 loi nghiem trong: eval() -> safe parser, Contract::saved() infinite loop, DB Transaction for schedule generation
This commit is contained in:
@@ -7,63 +7,67 @@ use App\Models\PaymentSchedule;
|
||||
use App\Models\PaymentScheduleItem;
|
||||
use App\Models\PaymentTemplate;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ContractScheduleService
|
||||
{
|
||||
/**
|
||||
* Tạo lịch thanh toán cho hợp đồng dựa trên mẫu.
|
||||
* Nếu đã tồn tại lịch cũ, sẽ xóa và tạo lại.
|
||||
* Toàn bộ quá trình được bọc trong DB Transaction để đảm bảo tính toàn vẹn.
|
||||
*/
|
||||
public static function generateFromTemplate(Contract $contract, ?PaymentTemplate $template = null): PaymentSchedule
|
||||
{
|
||||
if (! $template) {
|
||||
// Ưu tiên template của dự án
|
||||
$template = $contract->product?->project?->paymentTemplate;
|
||||
}
|
||||
|
||||
if (! $template) {
|
||||
throw new \InvalidArgumentException('Không tìm thấy mẫu thanh toán cho hợp đồng này.');
|
||||
}
|
||||
|
||||
// Xóa lịch cũ nếu có
|
||||
if ($contract->paymentSchedule) {
|
||||
$contract->paymentSchedule->items()->delete();
|
||||
$contract->paymentSchedule->delete();
|
||||
}
|
||||
|
||||
$schedule = PaymentSchedule::create([
|
||||
'contract_id' => $contract->id,
|
||||
'template_id' => $template->id,
|
||||
]);
|
||||
|
||||
$items = $template->items()->orderBy('installment_no')->get();
|
||||
$lastDueDate = Carbon::parse($contract->signing_date);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$dueDate = null;
|
||||
|
||||
if ($item->days_after_signing !== null) {
|
||||
$dueDate = Carbon::parse($contract->signing_date)->addDays($item->days_after_signing);
|
||||
} elseif ($item->days_after_previous !== null) {
|
||||
$dueDate = $lastDueDate->copy()->addDays($item->days_after_previous);
|
||||
} elseif ($item->due_date !== null) {
|
||||
$dueDate = $item->due_date;
|
||||
return DB::transaction(function () use ($contract, $template) {
|
||||
if (! $template) {
|
||||
// Ưu tiên template của dự án
|
||||
$template = $contract->product?->project?->paymentTemplate;
|
||||
}
|
||||
|
||||
PaymentScheduleItem::create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'installment_no' => $item->installment_no,
|
||||
'type' => $item->type,
|
||||
'percentage' => $item->percentage,
|
||||
'amount' => $contract->total_value * ($item->percentage / 100),
|
||||
'due_date' => $dueDate,
|
||||
if (! $template) {
|
||||
throw new \InvalidArgumentException('Không tìm thấy mẫu thanh toán cho hợp đồng này.');
|
||||
}
|
||||
|
||||
// Xóa lịch cũ nếu có
|
||||
if ($contract->paymentSchedule) {
|
||||
$contract->paymentSchedule->items()->delete();
|
||||
$contract->paymentSchedule->delete();
|
||||
}
|
||||
|
||||
$schedule = PaymentSchedule::create([
|
||||
'contract_id' => $contract->id,
|
||||
'template_id' => $template->id,
|
||||
]);
|
||||
|
||||
if ($dueDate) {
|
||||
$lastDueDate = $dueDate;
|
||||
}
|
||||
}
|
||||
$items = $template->items()->orderBy('installment_no')->get();
|
||||
$lastDueDate = Carbon::parse($contract->signing_date);
|
||||
|
||||
return $schedule;
|
||||
foreach ($items as $item) {
|
||||
$dueDate = null;
|
||||
|
||||
if ($item->days_after_signing !== null) {
|
||||
$dueDate = Carbon::parse($contract->signing_date)->addDays($item->days_after_signing);
|
||||
} elseif ($item->days_after_previous !== null) {
|
||||
$dueDate = $lastDueDate->copy()->addDays($item->days_after_previous);
|
||||
} elseif ($item->due_date !== null) {
|
||||
$dueDate = $item->due_date;
|
||||
}
|
||||
|
||||
PaymentScheduleItem::create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'installment_no' => $item->installment_no,
|
||||
'type' => $item->type,
|
||||
'percentage' => $item->percentage,
|
||||
'amount' => $contract->total_value * ($item->percentage / 100),
|
||||
'due_date' => $dueDate,
|
||||
]);
|
||||
|
||||
if ($dueDate) {
|
||||
$lastDueDate = $dueDate;
|
||||
}
|
||||
}
|
||||
|
||||
return $schedule;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,35 +72,116 @@ class MailMergeService
|
||||
$evalExpression = $expression;
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_numeric($value)) {
|
||||
$evalExpression = str_replace($key, (float) $value, $evalExpression);
|
||||
$evalExpression = str_replace($key, $value, $evalExpression);
|
||||
}
|
||||
}
|
||||
|
||||
// Chỉ cho phép số và các phép toán cơ bản
|
||||
// Chỉ cho phép số, dấu chấm, dấu phẩy và các phép toán cơ bản
|
||||
$evalExpression = str_replace(',', '.', $evalExpression);
|
||||
$evalExpression = preg_replace('/[^0-9.\+\-\*\/\(\)\s]/', '', $evalExpression);
|
||||
$evalExpression = str_replace(' ', '', $evalExpression);
|
||||
|
||||
if (empty($evalExpression)) return 0;
|
||||
|
||||
try {
|
||||
// Eval an toàn với chỉ phép toán
|
||||
$result = self::safeEval($evalExpression);
|
||||
$result = self::safeCalculate($evalExpression);
|
||||
return (float) $result;
|
||||
} catch (\Throwable $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected static function safeEval(string $expression): float
|
||||
protected static function safeCalculate(string $expression): float
|
||||
{
|
||||
// Loại bỏ các hàm nguy hiểm, chỉ giữ phép toán
|
||||
$expression = preg_replace('/[^0-9.\+\-\*\/\(\)\s]/', '', $expression);
|
||||
// Tokenize: tách số và operators
|
||||
$tokens = [];
|
||||
$number = '';
|
||||
|
||||
for ($i = 0; $i < strlen($expression); $i++) {
|
||||
$char = $expression[$i];
|
||||
|
||||
if (ctype_digit($char) || $char === '.') {
|
||||
$number .= $char;
|
||||
} else {
|
||||
if ($number !== '') {
|
||||
$tokens[] = (float) $number;
|
||||
$number = '';
|
||||
}
|
||||
$tokens[] = $char;
|
||||
}
|
||||
}
|
||||
|
||||
if ($number !== '') {
|
||||
$tokens[] = (float) $number;
|
||||
}
|
||||
|
||||
if (empty($expression) || preg_match('/[a-zA-Z]/', $expression)) {
|
||||
// Shunting yard algorithm: infix → postfix
|
||||
$output = [];
|
||||
$stack = [];
|
||||
$precedence = ['+' => 1, '-' => 1, '*' => 2, '/' => 2];
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (is_numeric($token)) {
|
||||
$output[] = $token;
|
||||
} elseif ($token === '(') {
|
||||
$stack[] = $token;
|
||||
} elseif ($token === ')') {
|
||||
while (!empty($stack) && end($stack) !== '(') {
|
||||
$output[] = array_pop($stack);
|
||||
}
|
||||
array_pop($stack); // pop '('
|
||||
} else {
|
||||
// Operator
|
||||
while (!empty($stack) && end($stack) !== '(' &&
|
||||
isset($precedence[end($stack)]) &&
|
||||
$precedence[end($stack)] >= $precedence[$token]) {
|
||||
$output[] = array_pop($stack);
|
||||
}
|
||||
$stack[] = $token;
|
||||
}
|
||||
}
|
||||
|
||||
while (!empty($stack)) {
|
||||
$output[] = array_pop($stack);
|
||||
}
|
||||
|
||||
// Evaluate postfix
|
||||
$evalStack = [];
|
||||
|
||||
foreach ($output as $token) {
|
||||
if (is_numeric($token)) {
|
||||
$evalStack[] = $token;
|
||||
} else {
|
||||
$b = array_pop($evalStack);
|
||||
$a = array_pop($evalStack);
|
||||
|
||||
if ($a === null || $b === null) {
|
||||
throw new \InvalidArgumentException('Invalid expression');
|
||||
}
|
||||
|
||||
switch ($token) {
|
||||
case '+':
|
||||
$evalStack[] = bcadd((string) $a, (string) $b, 10);
|
||||
break;
|
||||
case '-':
|
||||
$evalStack[] = bcsub((string) $a, (string) $b, 10);
|
||||
break;
|
||||
case '*':
|
||||
$evalStack[] = bcmul((string) $a, (string) $b, 10);
|
||||
break;
|
||||
case '/':
|
||||
if ((float) $b == 0) throw new \InvalidArgumentException('Division by zero');
|
||||
$evalStack[] = bcdiv((string) $a, (string) $b, 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($evalStack) !== 1) {
|
||||
throw new \InvalidArgumentException('Invalid expression');
|
||||
}
|
||||
|
||||
// Dùng bc math nếu có, hoặc eval đơn giản
|
||||
return (float) eval('return ' . $expression . ';');
|
||||
return (float) $evalStack[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user