invoiceninja/app/Http/Requests/TaskScheduler/PaymentScheduleRequest.php

156 lines
6.5 KiB
PHP

<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\TaskScheduler;
use App\Http\Requests\Request;
use App\Models\RecurringInvoice;
use App\Models\RecurringQuote;
use Illuminate\Support\Carbon;
class PaymentScheduleRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return auth()->user()->can('edit', $this->invoice);
}
public function rules()
{
return [
'next_run' => 'required|date:Y-m-d',
'frequency_id' => 'sometimes|integer|required_with:remaining_cycles',
'remaining_cycles' => 'sometimes|integer|required_with:frequency_id',
'parameters' => 'bail|array',
'parameters.schedule' => 'array|required_without:frequency_id,remaining_cycles',
'parameters.schedule.*.id' => 'required|integer',
'parameters.schedule.*.date' => 'required|date:Y-m-d',
'parameters.schedule.*.amount' => 'required|numeric',
'parameters.schedule.*.is_amount' => 'required|boolean',
'parameters.invoice_id' => 'required|string',
'parameters.auto_bill' => 'required|boolean',
];
}
public function prepareForValidation()
{
$input = $this->all();
$input['parameters']['invoice_id'] = $this->invoice->hashed_id;
$input['template'] = 'payment_schedule';
$input['name'] = "Payment Schedule for Invoice #{$this->invoice->number}";
$input['is_paused'] = false;
$input['parameters']['auto_bill'] = (bool) isset($input['parameters']['auto_bill']) ? $input['parameters']['auto_bill'] : false;
$input['parameters']['schedule'] = [];
if(isset($input['parameters']['schedule']) && is_array($input['parameters']['schedule']) && count($input['parameters']['schedule']) > 0) {
$input['parameters']['schedule'] = $input['parameters']['schedule'];
}
if (isset($input['schedule']) && is_array($input['schedule']) && count($input['schedule']) > 0) {
$schedule_map = collect($input['schedule'])->map(function ($schedule, $key) {
return [
'id' => $key,
'date' => $schedule['date'],
'amount' => $schedule['amount'],
'is_amount' => $schedule['is_amount'],
];
});
$first_map = $schedule_map->first();
if ($first_map['is_amount'] && floatval($schedule_map->sum('amount')) != floatval($this->invoice->amount)) {
$validator = \Validator::make([], []);
$validator->errors()->add('schedule', 'The total amount of the schedule does not match the invoice amount.');
throw new \Illuminate\Validation\ValidationException($validator);
}
elseif(!$first_map['is_amount'] && floatval($schedule_map->sum('amount')) != floatval(100)) {
$validator = \Validator::make([], []);
$validator->errors()->add('schedule', 'The total percentage amount of the schedule does not match 100%.');
throw new \Illuminate\Validation\ValidationException($validator);
}
else{
$input['parameters']['schedule'] = $schedule_map->toArray();
}
}
if (isset($input['frequency_id']) && isset($input['remaining_cycles'])) {
$due_date = $input['next_run'] ?? $this->invoice->due_date ?? Carbon::parse($this->invoice->date)->addDays((int)$this->invoice->client->getSetting('payment_terms'));
$input['parameters']['schedule'] = $this->generateSchedule($input['frequency_id'], $input['remaining_cycles'], Carbon::parse($due_date));
}
$input['remaining_cycles'] = count($input['parameters']['schedule']);
$input['next_run_client'] = $input['next_run'];
$input['next_run'] = Carbon::parse($input['next_run'])->addSeconds($this->invoice->company->timezone_offset())->format('Y-m-d');
$this->replace($input);
}
private function generateSchedule(int $frequency_id, int $remaining_cycles, Carbon $due_date)
{
$amount = round($this->invoice->amount / $remaining_cycles, 2);
$delta = round($amount * $remaining_cycles, 2);
$adjustment = 0;
if(floatval($delta) != floatval($this->invoice->amount)) {
$adjustment = round(floatval($this->invoice->amount) - floatval($delta), 2); //adjustment to make the total amount equal to the invoice amount
}
$schedule = [];
for ($i = 0; $i < $remaining_cycles; $i++) {
$schedule[] = [
'id' => $i+1,
'date' => $i === 0 ? $due_date->format('Y-m-d') : $this->generateScheduleByFrequency($frequency_id, $due_date)->format('Y-m-d'),
'amount' => $amount,
'is_amount' => true,
];
}
if($adjustment != 0) {
$schedule[$remaining_cycles-1]['amount'] += $adjustment;
}
return $schedule;
}
private function generateScheduleByFrequency(int $frequency_id, Carbon $date)
{
return match($frequency_id) {
RecurringInvoice::FREQUENCY_DAILY => $date->startOfDay()->addDay(),
RecurringInvoice::FREQUENCY_WEEKLY => $date->startOfDay()->addWeek(),
RecurringInvoice::FREQUENCY_TWO_WEEKS => $date->startOfDay()->addWeeks(2),
RecurringInvoice::FREQUENCY_FOUR_WEEKS => $date->startOfDay()->addWeeks(4),
RecurringInvoice::FREQUENCY_MONTHLY => $date->startOfDay()->addMonthNoOverflow(),
RecurringInvoice::FREQUENCY_TWO_MONTHS => $date->startOfDay()->addMonthsNoOverflow(2),
RecurringInvoice::FREQUENCY_THREE_MONTHS => $date->startOfDay()->addMonthsNoOverflow(3),
RecurringInvoice::FREQUENCY_FOUR_MONTHS => $date->startOfDay()->addMonthsNoOverflow(4),
RecurringInvoice::FREQUENCY_SIX_MONTHS => $date->startOfDay()->addMonthsNoOverflow(6),
RecurringInvoice::FREQUENCY_ANNUALLY => $date->startOfDay()->addYear(),
RecurringInvoice::FREQUENCY_TWO_YEARS => $date->startOfDay()->addYears(2),
RecurringInvoice::FREQUENCY_THREE_YEARS => $date->startOfDay()->addYears(3),
};
}
}