diff --git a/app/Filters/QueryFilters.php b/app/Filters/QueryFilters.php index fc33f283a554..036b7a892ed0 100644 --- a/app/Filters/QueryFilters.php +++ b/app/Filters/QueryFilters.php @@ -288,6 +288,33 @@ abstract class QueryFilters return $this->builder; } + /** + * @return Builder + * @throws RuntimeException + */ + public function without_deleted_clients(): Builder + { + return $this->builder->where(function ($query) { + $query->whereHas('client', function ($sub_query) { + $sub_query->where('is_deleted', 0); + })->orWhere('client_id', null); + }); + } + + /** + * @return Builder + * @throws RuntimeException + */ + public function without_deleted_vendors(): Builder + { + return $this->builder->where(function ($query) { + $query->whereHas('vendor', function ($sub_query) { + $sub_query->where('is_deleted', 0); + })->orWhere('vendor_id', null); + }); + } + + public function with(string $value = ''): Builder { if (strlen($value) == 0) { diff --git a/app/Http/ValidationRules/Credit/ValidCreditsRules.php b/app/Http/ValidationRules/Credit/ValidCreditsRules.php index 7b6fc32aed88..b0892da7330e 100644 --- a/app/Http/ValidationRules/Credit/ValidCreditsRules.php +++ b/app/Http/ValidationRules/Credit/ValidCreditsRules.php @@ -69,13 +69,16 @@ class ValidCreditsRules implements Rule if (! $cred) { $this->error_msg = ctrans('texts.credit_not_found'); - return false; } if ($cred->client_id != $this->input['client_id']) { $this->error_msg = ctrans('texts.invoices_dont_match_client'); + return false; + } + if($cred->balance < $credit['amount']) { + $this->error_msg = ctrans('texts.insufficient_credit_balance'); return false; } } diff --git a/app/Services/Invoice/HandleRestore.php b/app/Services/Invoice/HandleRestore.php index 4dc9621f4acf..c3e2947e8732 100644 --- a/app/Services/Invoice/HandleRestore.php +++ b/app/Services/Invoice/HandleRestore.php @@ -44,7 +44,6 @@ class HandleRestore extends AbstractService //cannot restore an invoice with a deleted payment foreach ($this->invoice->payments as $payment) { if (($this->invoice->paid_to_date == 0) && $payment->is_deleted) { - $this->invoice->delete(); //set it back to deleted so that it can be restored from repository return $this->invoice; } } @@ -81,8 +80,8 @@ class HandleRestore extends AbstractService Paymentable::query() ->withTrashed() ->where('payment_id', $payment->id) - ->where('paymentable_type', '=', 'invoices') - ->where('paymentable_id', $this->invoice->id) + // ->where('paymentable_type', '=', 'invoices') + // ->where('paymentable_id', $this->invoice->id) ->update(['deleted_at' => null]); }); @@ -102,6 +101,12 @@ class HandleRestore extends AbstractService ->where('paymentable_type', '=', 'invoices') ->where('paymentable_id', $this->invoice->id) ->sum(DB::raw('refunded')); + + //14/07/2023 - do not include credits in the payment amount + $this->adjustment_amount -= $payment->paymentables + ->where('paymentable_type', '=', 'App\Models\Credit') + ->sum(DB::raw('amount')); + } $this->total_payments = $this->invoice->payments->sum('amount') - $this->invoice->payments->sum('refunded'); @@ -130,11 +135,16 @@ class HandleRestore extends AbstractService ->where('paymentable_id', $this->invoice->id) ->sum(DB::raw('refunded')); + $payment_adjustment -= $payment->paymentables + ->where('paymentable_type', '=', 'App\Models\Credit') + ->sum(DB::raw('amount')); + $payment->amount += $payment_adjustment; $payment->applied += $payment_adjustment; $payment->is_deleted = false; $payment->restore(); $payment->saveQuietly(); + }); return $this; diff --git a/app/Services/Invoice/MarkInvoiceDeleted.php b/app/Services/Invoice/MarkInvoiceDeleted.php index 27291123d56e..72cfe0136de3 100644 --- a/app/Services/Invoice/MarkInvoiceDeleted.php +++ b/app/Services/Invoice/MarkInvoiceDeleted.php @@ -11,11 +11,12 @@ namespace App\Services\Invoice; -use App\Jobs\Inventory\AdjustProductInventory; +use App\Models\Credit; use App\Models\Invoice; use App\Services\AbstractService; -use App\Utils\Traits\GeneratesCounter; use Illuminate\Support\Facades\DB; +use App\Utils\Traits\GeneratesCounter; +use App\Jobs\Inventory\AdjustProductInventory; class MarkInvoiceDeleted extends AbstractService { @@ -94,6 +95,11 @@ class MarkInvoiceDeleted extends AbstractService ->where('paymentable_id', $this->invoice->id) ->sum(DB::raw('refunded')); + //14-07-2023 - Do not include credits in the payment adjustment. + $payment_adjustment -= $payment->paymentables + ->where('paymentable_type', '=', 'App\Models\Credit') + ->sum(DB::raw('amount')); + $payment->amount -= $payment_adjustment; $payment->applied -= $payment_adjustment; $payment->save(); diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index f5c02d1e86e5..fb50797b64f5 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -109,6 +109,8 @@ class DeletePayment $paymentable_invoice->service() ->updatePaidToDate($net_deletable * -1) ->save(); + $paymentable_invoice->delete(); + } }); } diff --git a/tests/Feature/Export/ExportCompanyTest.php b/tests/Feature/Export/ExportCompanyTest.php index 88696330ff63..c96c7fb347fc 100644 --- a/tests/Feature/Export/ExportCompanyTest.php +++ b/tests/Feature/Export/ExportCompanyTest.php @@ -46,7 +46,7 @@ class ExportCompanyTest extends TestCase public function testCompanyExport() { - $res = (new CompanyExport($this->company, $this->company->users->first()))->handle(); + $res = (new CompanyExport($this->company, $this->company->users->first(), '123'))->handle(); $this->assertTrue($res); } diff --git a/tests/Feature/PaymentV2Test.php b/tests/Feature/PaymentV2Test.php new file mode 100644 index 000000000000..761c00b03361 --- /dev/null +++ b/tests/Feature/PaymentV2Test.php @@ -0,0 +1,192 @@ +faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + $this->withoutExceptionHandling(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + } + + public function testStorePaymentWithCreditsThenDeletingInvoices() + { + $client = Client::factory()->create(['company_id' =>$this->company->id, 'user_id' => $this->user->id, 'balance' => 20, 'paid_to_date' => 0]); + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 20, + 'balance' => 20, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => [] + ]); + + $this->assertEquals(20, $client->balance); + $this->assertEquals(0, $client->paid_to_date); + $this->assertEquals(20, $invoice->amount); + $this->assertEquals(20, $invoice->balance); + + $credit = Credit::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 20, + 'balance' => 20, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => [] + ]); + + $this->assertEquals(20, $credit->amount); + $this->assertEquals(20, $credit->balance); + + $data = [ + 'client_id' => $client->hashed_id, + 'invoices' => [ + [ + 'invoice_id' => $invoice->hashed_id, + 'amount' => 20, + ], + ], + 'credits' => [ + [ + 'credit_id' => $credit->hashed_id, + 'amount' => 20, + ], + ], + 'date' => '2020/12/12', + + ]; + + $response = null; + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/payments?include=invoices', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + $this->assertNotNull($message); + } + + $arr = $response->json(); + $response->assertStatus(200); + + $payment_id = $arr['data']['id']; + + $payment = Payment::find($this->decodePrimaryKey($payment_id)); + + $this->assertNotNull($payment); + $this->assertNotNull($payment->invoices()); + $this->assertEquals(1, $payment->invoices()->count()); + $this->assertEquals(0, $payment->amount); + $this->assertEquals(0, $client->fresh()->balance); + $this->assertEquals(20, $client->fresh()->paid_to_date); + + $data = [ + 'action' => 'delete', + 'ids' => [ + $invoice->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/bulk', $data); + + $response->assertStatus(200); + + $invoice = $invoice->fresh(); + $payment = $payment->fresh(); + + $this->assertEquals(true, $invoice->is_deleted); + $this->assertEquals(0, $payment->amount); + $this->assertEquals(0, $client->fresh()->balance); + $this->assertEquals(0, $client->fresh()->paid_to_date); + + $data = [ + 'action' => 'restore', + 'ids' => [ + $invoice->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/bulk', $data); + + $invoice = $invoice->fresh(); + $this->assertEquals(false, $invoice->is_deleted); + + $payment = $payment->fresh(); + + $this->assertEquals(0, $payment->amount); + $this->assertEquals(20, $client->fresh()->paid_to_date); + + } +} \ No newline at end of file diff --git a/tests/Unit/PaymentTypeTest.php b/tests/Unit/PaymentTypeTest.php index 92440ad31efd..326e972627ad 100644 --- a/tests/Unit/PaymentTypeTest.php +++ b/tests/Unit/PaymentTypeTest.php @@ -33,7 +33,7 @@ class PaymentTypeTest extends TestCase $payment_type_class = new PaymentType; foreach($payment_type_class->type_names as $type) - {nlog($type); + { $this->assertTrue(Lang::has("texts.{$type}")); } }