diff --git a/app/Http/Requests/Credit/StoreCreditRequest.php b/app/Http/Requests/Credit/StoreCreditRequest.php index eb924c22f6eb..9baf0c706f44 100644 --- a/app/Http/Requests/Credit/StoreCreditRequest.php +++ b/app/Http/Requests/Credit/StoreCreditRequest.php @@ -94,6 +94,10 @@ class StoreCreditRequest extends Request $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['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index 79b5b59009aa..9dafd1583eb3 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -86,6 +86,10 @@ class UpdateCreditRequest extends Request $input = $this->decodePrimaryKeys($input); + if(isset($input['partial']) && $input['partial'] == 0) { + $input['partial_due_date'] = null; + } + if (isset($input['line_items'])) { $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index f467277d6775..f91faa9cc1f6 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -93,8 +93,8 @@ class StoreInvoiceRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } - if(isset($input['partial']) && $input['partial'] == 0 && isset($input['partial_due_date'])) { - $input['partial_due_date'] = ''; + if(isset($input['partial']) && $input['partial'] == 0) { + $input['partial_due_date'] = null; } $input['amount'] = 0; diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 9f752b24187e..f21feb1933f6 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -91,8 +91,8 @@ class UpdateInvoiceRequest extends Request $input['id'] = $this->invoice->id; - if(isset($input['partial']) && $input['partial'] == 0 && isset($input['partial_due_date'])) { - $input['partial_due_date'] = ''; + if(isset($input['partial']) && $input['partial'] == 0) { + $input['partial_due_date'] = null; } if (isset($input['line_items']) && is_array($input['line_items'])) { diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 735edd39b728..d21c73cbf68d 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -79,6 +79,10 @@ class StorePurchaseOrderRequest extends Request $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'])) { $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index 2bec0064e445..a9be48fb7f18 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -83,6 +83,10 @@ class UpdatePurchaseOrderRequest extends Request $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'])) { $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index e3f5e841442d..3fa933356469 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -43,7 +43,7 @@ class StoreQuoteRequest extends Request $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'))) { $rules['documents.*'] = $this->fileValidation(); @@ -64,12 +64,17 @@ class StoreQuoteRequest extends Request $rules['is_amount_discount'] = ['boolean']; $rules['exchange_rate'] = 'bail|sometimes|numeric'; $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; } public function prepareForValidation() { + /** @var \App\Models\User $user */ + $user = auth()->user(); + $input = $this->all(); $input = $this->decodePrimaryKeys($input); @@ -82,6 +87,19 @@ class StoreQuoteRequest extends Request $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); } } diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php index dc9a8d0affa8..4a8b386c9206 100644 --- a/app/Http/Requests/Quote/UpdateQuoteRequest.php +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -56,16 +56,16 @@ class UpdateQuoteRequest extends Request $rules['file'] = $this->fileValidation(); } - $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['line_items'] = 'array'; $rules['discount'] = 'sometimes|numeric'; $rules['is_amount_discount'] = ['boolean']; $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; } @@ -89,6 +89,9 @@ class UpdateQuoteRequest extends Request $input['exchange_rate'] = 1; } + if(isset($input['partial']) && $input['partial'] == 0) { + $input['partial_due_date'] = null; + } $this->replace($input); } diff --git a/app/Models/Quote.php b/app/Models/Quote.php index ca75f7aa5e9e..cf2d27afe2e2 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -77,7 +77,7 @@ use Laracasts\Presenter\PresentableTrait; * @property float $amount * @property float $balance * @property float|null $partial - * @property string|null $partial_due_date + * @property \Carbon\Carbon|null $partial_due_date * @property string|null $last_viewed * @property int|null $created_at * @property int|null $updated_at @@ -195,10 +195,10 @@ class Quote extends BaseModel return $this->dateMutator($value); } - public function getDueDateAttribute($value) - { - return $value ? $this->dateMutator($value) : null; - } +// public function getDueDateAttribute($value) +// { +// return $value ? $this->dateMutator($value) : null; +// } // public function getPartialDueDateAttribute($value) // { diff --git a/app/Transformers/QuoteTransformer.php b/app/Transformers/QuoteTransformer.php index adbf2467e36c..f918a90653e8 100644 --- a/app/Transformers/QuoteTransformer.php +++ b/app/Transformers/QuoteTransformer.php @@ -111,7 +111,7 @@ class QuoteTransformer extends EntityTransformer 'reminder2_sent' => $quote->reminder2_sent ?: '', 'reminder3_sent' => $quote->reminder3_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 ?: '', 'public_notes' => $quote->public_notes ?: '', 'private_notes' => $quote->private_notes ?: '', @@ -127,7 +127,7 @@ class QuoteTransformer extends EntityTransformer 'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false), 'footer' => $quote->footer ?: '', '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_value2' => (string) $quote->custom_value2 ?: '', 'custom_value3' => (string) $quote->custom_value3 ?: '', diff --git a/tests/Feature/QuoteTest.php b/tests/Feature/QuoteTest.php index 921cc0bdb57c..25c6639e4b37 100644 --- a/tests/Feature/QuoteTest.php +++ b/tests/Feature/QuoteTest.php @@ -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() { $data = [ 'client_id' => $this->client->hashed_id, - 'due_date' => now()->format('Y-m-d'), + 'due_date' => now()->addDay()->format('Y-m-d'), ]; $response = $this->withHeaders([ @@ -73,6 +160,41 @@ class QuoteTest extends TestCase $this->assertNotNull($arr['data']['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()