diff --git a/app/Http/Controllers/OpenAPI/ClientSchema.php b/app/Http/Controllers/OpenAPI/ClientSchema.php index b343e62b26b7..86049e5d2e1d 100644 --- a/app/Http/Controllers/OpenAPI/ClientSchema.php +++ b/app/Http/Controllers/OpenAPI/ClientSchema.php @@ -43,6 +43,7 @@ * @OA\Property(property="is_deleted", type="boolean", example=true, description="________"), * @OA\Property(property="balance", type="number", format="float", example="10.00", description="________"), * @OA\Property(property="paid_to_date", type="number", format="float", example="10.00", description="________"), + * @OA\Property(property="credit_balance", type="number", format="float", example="10.00", description="An amount which is available to the client for future use."), * @OA\Property(property="last_login", type="number", format="integer", example="134341234234", description="Timestamp"), * @OA\Property(property="created_at", type="number", format="integer", example="134341234234", description="Timestamp"), * @OA\Property(property="updated_at", type="number", format="integer", example="134341234234", description="Timestamp"), diff --git a/app/Http/Requests/Payment/StorePaymentRequest.php b/app/Http/Requests/Payment/StorePaymentRequest.php index 10987a8bd671..88bab4fb868e 100644 --- a/app/Http/Requests/Payment/StorePaymentRequest.php +++ b/app/Http/Requests/Payment/StorePaymentRequest.php @@ -33,15 +33,38 @@ class StorePaymentRequest extends Request } + protected function prepareForValidation() + { + + $input = $this->all(); + + if(isset($input['client_id'])) + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + + if(isset($input['invoices'])){ + + foreach($input['invoices'] as $key => $value) + { + $input['invoices'][$key]['id'] = $this->decodePrimaryKey($value['id']); + } + + } + + + if(is_array($input['invoices']) === false) + $input['invoices'] = null; + + $this->replace($input); + + } + public function rules() { - $this->sanitize(); - + $rules = [ 'amount' => 'numeric|required', 'payment_date' => 'required', 'client_id' => 'required', - 'invoices' => 'required', 'invoices' => new ValidPayableInvoicesRule(), ]; @@ -50,25 +73,4 @@ class StorePaymentRequest extends Request } - public function sanitize() - { - $input = $this->all(); - - if(isset($input['client_id'])) - $input['client_id'] = $this->decodePrimaryKey($input['client_id']); - - if(isset($input['invoices'])) - $input['invoices'] = $this->transformKeys(explode(",", $input['invoices'])); - - if(is_array($input['invoices']) === false) - $input['invoices'] = null; - - - $this->replace($input); - - return $this->all(); - - } - - } \ No newline at end of file diff --git a/app/Http/ValidationRules/ValidPayableInvoicesRule.php b/app/Http/ValidationRules/ValidPayableInvoicesRule.php index 8c8550d5e9ca..c6964b2f75cd 100644 --- a/app/Http/ValidationRules/ValidPayableInvoicesRule.php +++ b/app/Http/ValidationRules/ValidPayableInvoicesRule.php @@ -28,18 +28,23 @@ class ValidPayableInvoicesRule implements Rule * @param mixed $value * @return bool */ + + private $error_msg; + public function passes($attribute, $value) { + /*If no invoices has been sent, then we apply the payment to the client account*/ + $invoices = []; - $invoices = Invoice::whereIn('id', $this->transformKeys(explode(",",$value)))->get(); - - if(!$invoices || $invoices->count() == 0) - return false; + if(is_array($value)) + $invoices = Invoice::whereIn('id', array_column($value,'id'))->company()->get(); foreach ($invoices as $invoice) { - if(! $invoice->isPayable()) - return false; + if(! $invoice->isPayable()) { + $this->error_msg = "One or more of these invoices have been paid"; + return false; + } } return true; @@ -50,7 +55,7 @@ class ValidPayableInvoicesRule implements Rule */ public function message() { - return "One or more of these invoices have been paid"; + return $this->error_msg; } } diff --git a/app/Jobs/Invoice/ApplyClientPayment.php b/app/Jobs/Invoice/ApplyClientPayment.php new file mode 100644 index 000000000000..29969d49949c --- /dev/null +++ b/app/Jobs/Invoice/ApplyClientPayment.php @@ -0,0 +1,62 @@ +payment = $payment; + + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + + $client = $this->payment->client; + $client->credit_balance += $this->payment->amount; + $client->save(); + + } + +} \ No newline at end of file diff --git a/app/Jobs/Invoice/ApplyInvoicePayment.php b/app/Jobs/Invoice/ApplyInvoicePayment.php new file mode 100644 index 000000000000..86dcea30ad02 --- /dev/null +++ b/app/Jobs/Invoice/ApplyInvoicePayment.php @@ -0,0 +1,114 @@ +invoice = $invoice; + $this->payment = $payment; + $this->amount = $amount; + + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + + UpdateCompanyLedgerWithPayment::dispatchNow($this->payment, ($this->amount*-1)); + UpdateClientBalance::dispatchNow($this->payment->client, $this->amount*-1); + UpdateClientPaidToDate::dispatchNow($this->payment->client, $this->amount); + + /* Update Pivot Record amount */ + $this->payment->invoices->each(function ($inv){ + + if($inv->id == $this->invoice->id){ + $inv->pivot->amount = $this->amount; + $inv->pivot->save(); + } + + }); + + + if($this->invoice->hasPartial()) + { + //is partial and amount is exactly the partial amount + if($this->invoice->partial == $this->amount) + { + $this->invoice->clearPartial(); + $this->invoice->setDueDate(); + $this->invoice->setStatus(Invoice::STATUS_PARTIAL); + $this->invoice->updateBalance($this->amount*-1); + } + elseif($this->invoice->partial > 0 && $this->invoice->partial > $this->amount) //partial amount exists, but the amount is less than the partial amount + { + \Log::error("partial > amount"); + $this->invoice->partial -= $this->amount; + $this->invoice->updateBalance($this->amount*-1); + } + elseif($this->invoice->partial > 0 && $this->invoice->partial < $this->amount) //partial exists and the amount paid is GREATER than the partial amount + { + \Log::error("partial < amount"); + $this->invoice->clearPartial(); + $this->invoice->setDueDate(); + $this->invoice->setStatus(Invoice::STATUS_PARTIAL); + $this->invoice->updateBalance($this->amount*-1); + } + + } + elseif($this->invoice->amount == $this->invoice->balance) //total invoice paid. + { + \Log::error("balance == amount"); + + $this->invoice->clearPartial(); + $this->invoice->setDueDate(); + $this->invoice->setStatus(Invoice::STATUS_PAID); + $this->invoice->updateBalance($this->amount*-1); + } + + + } +} \ No newline at end of file diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 9e26c7e61cc2..55f2ddffcdb4 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -370,6 +370,7 @@ class Invoice extends BaseModel $balance_adjustment = floatval($balance_adjustment); + \Log::error("adjusting balance from ". $this->balance. " to ". ($this->balance + $balance_adjustment)); $this->balance = $this->balance + $balance_adjustment; if($this->balance == 0) { @@ -385,8 +386,7 @@ class Invoice extends BaseModel public function setDueDate() { - - $this->due_date = Carbon::now()->addDays(PaymentTerm::find($this->company->settings->payment_terms_id)->num_days); + $this->due_date = Carbon::now()->addDays($this->client->getSetting('payment_terms')); $this->save(); } diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index 6cc167c49c44..804c3e8c15a0 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -14,6 +14,8 @@ namespace App\Repositories; use App\Events\Payment\PaymentWasCreated; use App\Jobs\Company\UpdateCompanyLedgerWithPayment; use App\Jobs\Invoice\UpdateInvoicePayment; +use App\Jobs\Invoice\ApplyInvoicePayment; +use App\Jobs\Invoice\ApplyClientPayment; use App\Models\Invoice; use App\Models\Payment; use Illuminate\Http\Request; @@ -39,17 +41,31 @@ class PaymentRepository extends BaseRepository if($request->input('invoices')) { - $invoices = Invoice::whereIn('id', $request->input('invoices'))->get(); + $invoices = Invoice::whereIn('id', array_column($request->input('invoices'),'id'))->company()->get(); $payment->invoices()->saveMany($invoices); + foreach($request->input('invoices') as $paid_invoice) + { + + $invoice = Invoice::whereId($paid_invoice['id'])->company()->first(); + + if($invoice) + ApplyInvoicePayment::dispatchNow($invoice, $payment, $paid_invoice['amount']); + + } + + } + else { + //paid is made, but not to any invoice, therefore we are applying the payment to the clients credit + ApplyClientPayment::dispatchNow($payment); } event(new PaymentWasCreated($payment)); - UpdateInvoicePayment::dispatchNow($payment); + //UpdateInvoicePayment::dispatchNow($payment); - return $payment; + return $payment->fresh(); } diff --git a/app/Transformers/ClientTransformer.php b/app/Transformers/ClientTransformer.php index e65a9007eecf..2ac5f7e8546c 100644 --- a/app/Transformers/ClientTransformer.php +++ b/app/Transformers/ClientTransformer.php @@ -86,6 +86,7 @@ class ClientTransformer extends EntityTransformer 'balance' => (float) $client->balance, 'group_settings_id' => isset($client->group_settings_id) ? (string)$this->encodePrimaryKey($client->group_settings_id) : '', 'paid_to_date' => (float) $client->paid_to_date, + 'credit_balance' => (float) $client->credit_balance, 'last_login' => (int)$client->last_login, // 'currency_id' => (string)$client->currency_id, 'address1' => $client->address1 ?: '', diff --git a/database/migrations/2014_10_13_000000_create_users_table.php b/database/migrations/2014_10_13_000000_create_users_table.php index 0d46258c3aa1..0beb235efb09 100644 --- a/database/migrations/2014_10_13_000000_create_users_table.php +++ b/database/migrations/2014_10_13_000000_create_users_table.php @@ -295,6 +295,7 @@ class CreateUsersTable extends Migration $table->decimal('balance', 16, 4)->default(0); $table->decimal('paid_to_date', 16, 4)->default(0); + $table->decimal('credit_balance', 16, 4)->default(0); $table->timestamp('last_login')->nullable(); $table->unsignedInteger('industry_id')->nullable(); $table->unsignedInteger('size_id')->nullable(); diff --git a/tests/Feature/PaymentTest.php b/tests/Feature/PaymentTest.php index dbf743326459..09dd91d675c0 100644 --- a/tests/Feature/PaymentTest.php +++ b/tests/Feature/PaymentTest.php @@ -129,7 +129,12 @@ class PaymentTest extends TestCase $data = [ 'amount' => $this->invoice->amount, - 'invoices' => $this->invoice->hashed_id, + 'invoices' => [ + [ + 'id' => $this->invoice->hashed_id, + 'amount' => $this->invoice->amount + ], + ], 'payment_date' => '2020/12/11', ]; @@ -174,11 +179,17 @@ class PaymentTest extends TestCase $data = [ 'amount' => $this->invoice->amount, 'client_id' => $client->hashed_id, - 'invoices' => $this->invoice->hashed_id, + 'invoices' => [ + [ + 'id' => $this->invoice->hashed_id, + 'amount' => $this->invoice->amount + ], + ], 'payment_date' => '2020/12/12', ]; + $response = null; try { $response = $this->withHeaders([ @@ -194,12 +205,20 @@ class PaymentTest extends TestCase $this->assertNotNull($message); } - - $arr = $response->json(); - // \Log::error($arr); - $response->assertStatus(200); - + if($response){ + $arr = $response->json(); + $response->assertStatus(200); + + $payment_id = $arr['data']['id']; + + $payment = Payment::find($this->decodePrimaryKey($payment_id))->first(); + + $this->assertNotNull($payment); + $this->assertNotNull($payment->invoices()); + $this->assertEquals(1, $payment->invoices()->count()); + } + } public function testStorePaymentWithNoInvoiecs() @@ -212,7 +231,7 @@ class PaymentTest extends TestCase $this->invoice->status_id = Invoice::STATUS_SENT; $this->invoice->line_items = $this->buildLineItems(); - $this->invoice->uses_inclusive_Taxes = false; + $this->invoice->uses_inclusive_taxes = false; $this->invoice->save(); @@ -241,9 +260,72 @@ class PaymentTest extends TestCase catch(ValidationException $e) { $message = json_decode($e->validator->getMessageBag(),1); $this->assertNotNull($message); - } + if($response) + $response->assertStatus(200); + } + + public function testPartialPaymentAmount() + { + $this->invoice = null; + + $client = ClientFactory::create($this->company->id, $this->user->id); + $client->save(); + + $this->invoice = InvoiceFactory::create($this->company->id,$this->user->id);//stub the company and user_id + $this->invoice->client_id = $client->id; + + $this->invoice->partial = 2.0; + $this->invoice->line_items = $this->buildLineItems(); + $this->invoice->uses_inclusive_taxes = false; + + $this->invoice->save(); + + $this->invoice_calc = new InvoiceSum($this->invoice); + $this->invoice_calc->build(); + + $this->invoice = $this->invoice_calc->getInvoice(); + $this->invoice->save(); + $this->invoice->markSent(); + $this->invoice->save(); + + + $data = [ + 'amount' => 2.0, + 'client_id' => $client->hashed_id, + 'invoices' => [ + [ + 'id' => $this->invoice->hashed_id, + 'amount' => 2.0 + ], + ], + 'payment_date' => '2019/12/12', + ]; + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/payments?include=invoices', $data); + + $arr = $response->json(); + $response->assertStatus(200); + + $payment_id = $arr['data']['id']; + + $payment = Payment::find($this->decodePrimaryKey($payment_id))->first(); + + $this->assertNotNull($payment); + $this->assertNotNull($payment->invoices()); + $this->assertEquals(1, $payment->invoices()->count()); + + $pivot_invoice = $payment->invoices()->first(); + $this->assertEquals($pivot_invoice->pivot->amount, 2); + $this->assertEquals($pivot_invoice->partial, 0); + $this->assertEquals($pivot_invoice->amount, 10.0000); + $this->assertEquals($pivot_invoice->balance, 8.0000); + } }