feat(taxation): fresh tax-type request implementation

This commit is contained in:
Darko Gjorgjijoski
2026-08-20 21:00:15 +02:00
parent 00bcc940af
commit dd8cb9f274
@@ -0,0 +1,166 @@
<?php
namespace App\Domains\Taxation\Http\Requests;
use App\Domains\Taxation\Models\TaxType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
/**
* Incoming payload for creating or editing a hand-maintained tax type.
*
* Beyond the field rules it does two things: it decides whether the compound
* flag is allowed by looking at the values the row will *end up* with — the
* submitted ones, falling back to what is stored when an edit stays silent
* about a field — and it produces the attribute array to persist, pinned to
* the GENERAL kind and to the company the request was addressed to.
*/
class TaxTypeRequest extends FormRequest
{
/**
* Access is settled by the tax-type policy in the controller.
*/
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
$name = Rule::unique('tax_types')
->where('type', TaxType::TYPE_GENERAL)
->where('company_id', $this->header('company'));
// Quirk kept as is: only PUT excuses the edited row from the name
// check. A PATCH would collide with its own stored name.
if ($this->isMethod('PUT')) {
$name->ignore($this->route('tax_type')->id);
}
return [
'name' => [
'required',
$name,
],
'calculation_type' => [
'required',
Rule::in(['percentage', 'fixed']),
],
'percent' => [
'nullable',
'numeric',
],
'fixed_amount' => [
'nullable',
'numeric',
],
'description' => [
'nullable',
],
'compound_tax' => [
'sometimes',
'boolean',
],
'collective_tax' => [
'nullable',
],
'transaction_type' => [
'sometimes',
Rule::in([
TaxType::TRANSACTION_TYPE_SALES,
TaxType::TRANSACTION_TYPE_PURCHASES,
]),
],
];
}
/**
* Compounding is reserved for percentage taxes on the sales side.
*
* The check is skipped while any other rule has already failed, so a
* request with, say, a bad calculation type reports that alone.
*/
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator): void {
if ($validator->errors()->isNotEmpty() || ! $this->compoundFlag()) {
return;
}
$allowed = $this->settledValue('calculation_type', 'percentage') === 'percentage'
&& $this->settledValue('transaction_type', TaxType::TRANSACTION_TYPE_SALES)
=== TaxType::TRANSACTION_TYPE_SALES;
if (! $allowed) {
$validator->errors()->add(
'compound_tax',
'Compound tax is only available for percentage sales taxes.'
);
}
});
}
/**
* Attributes to write, with the gaps a legacy client leaves filled in.
*/
public function getTaxTypePayload()
{
$payload = $this->validated();
if (! array_key_exists('transaction_type', $payload)) {
$payload['transaction_type'] = $this->isEdit()
? $this->storedValue('transaction_type')
: TaxType::TRANSACTION_TYPE_SALES;
}
// An edit that omits the flag keeps whatever the row already holds;
// a create that omits it is plainly not compound.
if (! $this->isEdit() && ! array_key_exists('compound_tax', $payload)) {
$payload['compound_tax'] = false;
}
$payload['company_id'] = $this->header('company');
$payload['type'] = TaxType::TYPE_GENERAL;
return $payload;
}
/**
* Whether the row will carry the compound flag once written.
*/
private function compoundFlag(): bool
{
if ($this->has('compound_tax')) {
return $this->boolean('compound_tax');
}
return $this->isEdit() ? $this->storedValue('compound_tax') : false;
}
/**
* The value a field will hold after the write: what was sent, else the
* stored value on an edit, else the create-time default.
*/
private function settledValue(string $field, string $default): ?string
{
if ($this->has($field)) {
return $this->input($field);
}
return $this->isEdit() ? $this->storedValue($field) : $default;
}
private function storedValue(string $field)
{
return $this->route('tax_type')->{$field};
}
private function isEdit(): bool
{
return $this->isMethod('PUT') || $this->isMethod('PATCH');
}
}