Improvements for quote validation

This commit is contained in:
David Bomba 2024-04-15 09:14:11 +10:00
parent c472e8ce68
commit 67c80ecdd9
11 changed files with 175 additions and 16 deletions

View File

@ -94,6 +94,10 @@ class StoreCreditRequest extends Request
$input['design_id'] = $this->decodePrimaryKey($input['design_id']); $input['design_id'] = $this->decodePrimaryKey($input['design_id']);
} }
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];

View File

@ -86,6 +86,10 @@ class UpdateCreditRequest extends Request
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (isset($input['line_items'])) { if (isset($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
} }

View File

@ -93,8 +93,8 @@ class StoreInvoiceRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
} }
if(isset($input['partial']) && $input['partial'] == 0 && isset($input['partial_due_date'])) { if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = ''; $input['partial_due_date'] = null;
} }
$input['amount'] = 0; $input['amount'] = 0;

View File

@ -91,8 +91,8 @@ class UpdateInvoiceRequest extends Request
$input['id'] = $this->invoice->id; $input['id'] = $this->invoice->id;
if(isset($input['partial']) && $input['partial'] == 0 && isset($input['partial_due_date'])) { if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = ''; $input['partial_due_date'] = null;
} }
if (isset($input['line_items']) && is_array($input['line_items'])) { if (isset($input['line_items']) && is_array($input['line_items'])) {

View File

@ -79,6 +79,10 @@ class StorePurchaseOrderRequest extends Request
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (isset($input['line_items']) && is_array($input['line_items'])) { if (isset($input['line_items']) && is_array($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
} }

View File

@ -83,6 +83,10 @@ class UpdatePurchaseOrderRequest extends Request
$input['id'] = $this->purchase_order->id; $input['id'] = $this->purchase_order->id;
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (isset($input['line_items']) && is_array($input['line_items'])) { if (isset($input['line_items']) && is_array($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
} }

View File

@ -43,7 +43,7 @@ class StoreQuoteRequest extends Request
$rules = []; $rules = [];
$rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; $rules['client_id'] = ['required', 'bail', Rule::exists('clients','id')->where('company_id', $user->company()->id)];
if ($this->file('documents') && is_array($this->file('documents'))) { if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation(); $rules['documents.*'] = $this->fileValidation();
@ -64,12 +64,17 @@ class StoreQuoteRequest extends Request
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];
$rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date', 'after_or_equal:date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules; return $rules;
} }
public function prepareForValidation() public function prepareForValidation()
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
$input = $this->all(); $input = $this->all();
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
@ -82,6 +87,19 @@ class StoreQuoteRequest extends Request
$input['exchange_rate'] = 1; $input['exchange_rate'] = 1;
} }
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if(!isset($input['date']))
$input['date'] = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
if(isset($input['partial_due_date']) && (!isset($input['due_date']) || strlen($input['due_date']) <=1 )) {
$client = \App\Models\Client::withTrashed()->find($input['client_id']);
$valid_days = ($client && strlen($client->getSetting('valid_until')) >= 1) ? $client->getSetting('valid_until') : 7;
$input['due_date'] = \Carbon\Carbon::parse($input['date'])->addDays($valid_days)->format('Y-m-d');
}
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -56,16 +56,16 @@ class UpdateQuoteRequest extends Request
$rules['file'] = $this->fileValidation(); $rules['file'] = $this->fileValidation();
} }
$rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)->ignore($this->quote->id)]; $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)->ignore($this->quote->id)];
$rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->quote->client_id])]; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->quote->client_id])];
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric'; $rules['discount'] = 'sometimes|numeric';
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];
$rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', 'after_or_equal:date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules; return $rules;
} }
@ -89,6 +89,9 @@ class UpdateQuoteRequest extends Request
$input['exchange_rate'] = 1; $input['exchange_rate'] = 1;
} }
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
$this->replace($input); $this->replace($input);
} }

View File

@ -77,7 +77,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property float $amount * @property float $amount
* @property float $balance * @property float $balance
* @property float|null $partial * @property float|null $partial
* @property string|null $partial_due_date * @property \Carbon\Carbon|null $partial_due_date
* @property string|null $last_viewed * @property string|null $last_viewed
* @property int|null $created_at * @property int|null $created_at
* @property int|null $updated_at * @property int|null $updated_at
@ -195,10 +195,10 @@ class Quote extends BaseModel
return $this->dateMutator($value); return $this->dateMutator($value);
} }
public function getDueDateAttribute($value) // public function getDueDateAttribute($value)
{ // {
return $value ? $this->dateMutator($value) : null; // return $value ? $this->dateMutator($value) : null;
} // }
// public function getPartialDueDateAttribute($value) // public function getPartialDueDateAttribute($value)
// { // {

View File

@ -111,7 +111,7 @@ class QuoteTransformer extends EntityTransformer
'reminder2_sent' => $quote->reminder2_sent ?: '', 'reminder2_sent' => $quote->reminder2_sent ?: '',
'reminder3_sent' => $quote->reminder3_sent ?: '', 'reminder3_sent' => $quote->reminder3_sent ?: '',
'reminder_last_sent' => $quote->reminder_last_sent ?: '', 'reminder_last_sent' => $quote->reminder_last_sent ?: '',
'due_date' => $quote->due_date ?: '', 'due_date' => $quote->due_date ? $quote->due_date->format('Y-m-d') : '',
'terms' => $quote->terms ?: '', 'terms' => $quote->terms ?: '',
'public_notes' => $quote->public_notes ?: '', 'public_notes' => $quote->public_notes ?: '',
'private_notes' => $quote->private_notes ?: '', 'private_notes' => $quote->private_notes ?: '',
@ -127,7 +127,7 @@ class QuoteTransformer extends EntityTransformer
'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false), 'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false),
'footer' => $quote->footer ?: '', 'footer' => $quote->footer ?: '',
'partial' => (float) ($quote->partial ?: 0.0), 'partial' => (float) ($quote->partial ?: 0.0),
'partial_due_date' => $quote->partial_due_date ?: '', 'partial_due_date' => $quote->partial_due_date ? $quote->partial_due_date->format('Y-m-d') : '',
'custom_value1' => (string) $quote->custom_value1 ?: '', 'custom_value1' => (string) $quote->custom_value1 ?: '',
'custom_value2' => (string) $quote->custom_value2 ?: '', 'custom_value2' => (string) $quote->custom_value2 ?: '',
'custom_value3' => (string) $quote->custom_value3 ?: '', 'custom_value3' => (string) $quote->custom_value3 ?: '',

View File

@ -54,12 +54,99 @@ class QuoteTest extends TestCase
); );
} }
public function testQuoteDueDateInjectionValidationLayer()
{
$data = [
'client_id' => $this->client->hashed_id,
'partial_due_date' => now()->format('Y-m-d'),
'partial' => 1,
'amount' => 20,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/quotes', $data);
$arr = $response->json();
// nlog($arr);
$this->assertNotEmpty($arr['data']['due_date']);
}
public function testNullDueDates()
{
$data = [
'client_id' => $this->client->hashed_id,
'due_date' => '',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/quotes', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEmpty($arr['data']['due_date']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/quotes/'.$arr['data']['id'], $arr['data']);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEmpty($arr['data']['due_date']);
}
public function testNonNullDueDates()
{
$data = [
'client_id' => $this->client->hashed_id,
'due_date' => now()->addDays(10),
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/quotes', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertNotEmpty($arr['data']['due_date']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/quotes/'.$arr['data']['id'], $arr['data']);
$response->assertStatus(200);
$arr = $response->json();
$this->assertNotEmpty($arr['data']['due_date']);
}
public function testPartialDueDates() public function testPartialDueDates()
{ {
$data = [ $data = [
'client_id' => $this->client->hashed_id, 'client_id' => $this->client->hashed_id,
'due_date' => now()->format('Y-m-d'), 'due_date' => now()->addDay()->format('Y-m-d'),
]; ];
$response = $this->withHeaders([ $response = $this->withHeaders([
@ -73,6 +160,41 @@ class QuoteTest extends TestCase
$this->assertNotNull($arr['data']['due_date']); $this->assertNotNull($arr['data']['due_date']);
$this->assertEmpty($arr['data']['partial_due_date']); $this->assertEmpty($arr['data']['partial_due_date']);
$data = [
'client_id' => $this->client->hashed_id,
'due_date' => now()->addDay()->format('Y-m-d'),
'partial' => 1,
'partial_due_date' => now()->format('Y-m-d'),
'amount' => 20,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/quotes', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals(now()->addDay()->format('Y-m-d'), $arr['data']['due_date']);
$this->assertEquals(now()->format('Y-m-d'), $arr['data']['partial_due_date']);
$this->assertEquals(1, $arr['data']['partial']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/quotes/'.$arr['data']['id'], $arr['data']);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals(now()->addDay()->format('Y-m-d'), $arr['data']['due_date']);
$this->assertEquals(now()->format('Y-m-d'), $arr['data']['partial_due_date']);
$this->assertEquals(1, $arr['data']['partial']);
} }
public function testQuoteToProjectConversion2() public function testQuoteToProjectConversion2()