Merge pull request #8010 from turbo124/v5-develop

Bug Fix - Client balance corrupted when transaction is deleted.
This commit is contained in:
David Bomba 2022-11-30 15:42:01 +11:00 committed by GitHub
commit 002d7941be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 361 additions and 90 deletions

View File

@ -54,6 +54,7 @@ class RecurringInvoiceExport extends BaseExport
'po_number' => 'po_number',
'private_notes' => 'private_notes',
'public_notes' => 'public_notes',
'next_send_date' => 'next_send_date',
'status' => 'status_id',
'tax_name1' => 'tax_name1',
'tax_name2' => 'tax_name2',
@ -66,6 +67,7 @@ class RecurringInvoiceExport extends BaseExport
'currency' => 'currency_id',
'vendor' => 'vendor_id',
'project' => 'project_id',
'frequency' => 'frequency_id'
];
private array $decorate_keys = [
@ -162,6 +164,8 @@ class RecurringInvoiceExport extends BaseExport
$entity['vendor'] = $invoice->vendor ? $invoice->vendor->name : '';
}
$entity['frequency'] = $invoice->frequencyForKey($invoice->frequency_id);
return $entity;
}
}

View File

@ -26,7 +26,7 @@
* @OA\Property(property="city", type="string", example="", description="________"),
* @OA\Property(property="state", type="string", example="", description="________"),
* @OA\Property(property="postal_code", type="string", example="", description="________"),
* @OA\Property(property="work_phone", type="string", example="555-3434-3434", description="The client phone number"),
* @OA\Property(property="phone", type="string", example="555-3434-3434", description="The client phone number"),
* @OA\Property(property="country_id", type="string", example="", description="________"),
* @OA\Property(property="currency_id", type="string", example="4", description="________"),
* @OA\Property(property="custom_value1", type="string", example="", description="________"),

View File

@ -44,7 +44,7 @@ class StoreBankIntegrationRequest extends Request
{
$input = $this->all();
if(!array_key_exists('provider_name', $input) || strlen($input['provider_name']) == 0 && array_key_exists('bank_account_name', $input))
if((!array_key_exists('provider_name', $input) || strlen($input['provider_name']) == 0) && array_key_exists('bank_account_name', $input))
$input['provider_name'] = $input['bank_account_name'];
$this->replace($input);

View File

@ -34,8 +34,7 @@ class StoreBankTransactionRequest extends Request
$rules = [];
if(isset($this->bank_integration_id))
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules;
}
@ -44,7 +43,9 @@ class StoreBankTransactionRequest extends Request
{
$input = $this->all();
if(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1 && !is_numeric($input['bank_integration_id']))
if(array_key_exists('bank_integration_id', $input) && $input['bank_integration_id'] == "")
unset($input['bank_integration_id']);
elseif(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1 && !is_numeric($input['bank_integration_id']))
$input['bank_integration_id'] = $this->decodePrimaryKey($input['bank_integration_id']);
$this->replace($input);

View File

@ -45,8 +45,7 @@ class UpdateBankTransactionRequest extends Request
if(isset($this->expense_id))
$rules['expense_id'] = 'bail|required|exists:expenses,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->bank_integration_id))
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules;
@ -69,7 +68,9 @@ class UpdateBankTransactionRequest extends Request
if(array_key_exists('ninja_category_id', $input) && strlen($input['ninja_category_id']) > 1)
$input['ninja_category_id'] = $this->decodePrimaryKey($input['ninja_category_id']);
if(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1)
if(array_key_exists('bank_integration_id', $input) && $input['bank_integration_id'] == "")
unset($input['bank_integration_id']);
elseif(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1)
$input['bank_integration_id'] = $this->decodePrimaryKey($input['bank_integration_id']);
$this->replace($input);

View File

@ -38,7 +38,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->company->id,
'name' => $this->getString($data, 'client.name'),
'work_phone' => $this->getString($data, 'client.phone'),
'phone' => $this->getString($data, 'client.phone'),
'address1' => $this->getString($data, 'client.address1'),
'address2' => $this->getString($data, 'client.address2'),
'postal_code' => $this->getString($data, 'client.postal_code'),

View File

@ -42,7 +42,7 @@ class ClientTransformer extends BaseTransformer
'company_id' => $this->company->id,
'name' => $this->getString($data, 'customer_name'),
'number' => $this->getValueOrNull($data, 'account_number'),
'work_phone' => $this->getString($data, 'phone'),
'phone' => $this->getString($data, 'phone'),
'website' => $this->getString($data, 'website'),
'country_id' => ! empty($data['country']) ? $this->getCountryId($data['country']) : null,
'state' => $this->getString($data, 'province/state'),

View File

@ -35,7 +35,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'client.name'),
'work_phone' => $this->getString($data, 'client.phone'),
'phone' => $this->getString($data, 'client.phone'),
'address1' => $this->getString($data, 'client.address1'),
'address2' => $this->getString($data, 'client.address2'),
'city' => $this->getString($data, 'client.city'),

View File

@ -37,7 +37,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'client.name'),
'work_phone' => $this->getString($data, 'client.phone'),
'phone' => $this->getString($data, 'client.phone'),
'address1' => $this->getString($data, 'client.address1'),
'address2' => $this->getString($data, 'client.address2'),
'postal_code' => $this->getString($data, 'client.postal_code'),

View File

@ -34,7 +34,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'Organization'),
'work_phone' => $this->getString($data, 'Phone'),
'phone' => $this->getString($data, 'Phone'),
'address1' => $this->getString($data, 'Street'),
'city' => $this->getString($data, 'City'),
'state' => $this->getString($data, 'Province/State'),

View File

@ -34,7 +34,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'Client Name'),
'work_phone' => $this->getString($data, 'Phone'),
'phone' => $this->getString($data, 'Phone'),
'country_id' => isset($data['Country']) ? $this->getCountryIdBy2($data['Country']) : null,
'credit_balance' => 0,
'settings' => new \stdClass,

View File

@ -42,7 +42,7 @@ class ClientTransformer extends BaseTransformer
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'customer_name'),
'number' => $this->getString($data, 'account_number'),
'work_phone' => $this->getString($data, 'phone'),
'phone' => $this->getString($data, 'phone'),
'website' => $this->getString($data, 'website'),
'country_id' => ! empty($data['country']) ? $this->getCountryId($data['country']) : null,
'state' => $this->getString($data, 'province/state'),

View File

@ -41,7 +41,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'Company Name'),
'work_phone' => $this->getString($data, 'Phone'),
'phone' => $this->getString($data, 'Phone'),
'private_notes' => $this->getString($data, 'Notes'),
'website' => $this->getString($data, 'Website'),
'id_number' => $this->getString($data, 'Customer ID'),

View File

@ -266,7 +266,7 @@ class MatchBankTransactions implements ShouldQueue
/* Create Payment */
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
$payment->amount = $amount;
$payment->amount = $this->bt->amount;
$payment->applied = $this->applied_amount;
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->client_id = $this->invoice->client_id;
@ -315,7 +315,7 @@ class MatchBankTransactions implements ShouldQueue
$this->invoice
->client
->service()
->updateBalanceAndPaidToDate($amount*-1, $amount)
->updateBalanceAndPaidToDate($this->applied_amount*-1, $amount)
->save();
$this->invoice = $this->invoice

View File

@ -119,7 +119,7 @@ class CompanyPresenter extends EntityPresenter
$str .= e($country->name).'<br/>';
}
if ($settings->phone) {
$str .= ctrans('texts.work_phone').': '.e($settings->phone).'<br/>';
$str .= ctrans('texts.phone').': '.e($settings->phone).'<br/>';
}
if ($settings->email) {
$str .= ctrans('texts.work_email').': '.e($settings->email).'<br/>';

View File

@ -94,8 +94,6 @@ class CreditCard implements MethodInterface
$customerRequest = $this->checkout->getCustomer();
nlog($customerRequest);
$request = $this->bootRequest($gateway_response->token);
$request->capture = false;
$request->reference = '$1 payment for authorization.';

View File

@ -34,6 +34,7 @@ use Checkout\CheckoutArgumentException;
use Checkout\CheckoutAuthorizationException;
use Checkout\CheckoutDefaultSdk;
use Checkout\CheckoutFourSdk;
use Checkout\Common\Phone;
use Checkout\Customers\CustomerRequest;
use Checkout\Customers\Four\CustomerRequest as FourCustomerRequest;
use Checkout\Environment;
@ -300,9 +301,12 @@ class CheckoutComPaymentDriver extends BaseDriver
$request = new CustomerRequest();
}
$request->email = $this->client->present()->email();
$request->name = $this->client->present()->name();
$request->phone = $this->client->present()->phone();
$phone = new Phone();
$phone->number = $this->client->present()->phone();
$request->email = $this->client->present()->email();
$request->name = $this->client->present()->name();
$request->phone = $phone;
try {
$response = $this->gateway->getCustomersClient()->create($request);

View File

@ -66,11 +66,9 @@ class PaymentIntentWebhook implements ShouldQueue
{
$payment = Payment::query()
->where('company_id', $company->id)
->where(function ($query) use ($transaction) {
$query->where('transaction_reference', $transaction['payment_intent'])
->orWhere('transaction_reference', $transaction['id']);
})
->where('transaction_reference', $transaction['payment_intent'])
->first();
}
else
{

View File

@ -56,6 +56,8 @@ class ClientRepository extends BaseRepository
*/
public function save(array $data, Client $client) : ?Client
{
$contact_data = $data;
unset($data['contacts']);
/* When uploading documents, only the document array is sent, so we must return early*/
if (array_key_exists('documents', $data) && count($data['documents']) >= 1) {
@ -67,7 +69,7 @@ class ClientRepository extends BaseRepository
$client->fill($data);
if (array_key_exists('settings', $data)) {
$client->saveSettings($data['settings'], $client);
$client->settings = $client->saveSettings($data['settings'], $client);
}
if (! $client->country_id) {
@ -75,19 +77,9 @@ class ClientRepository extends BaseRepository
$client->country_id = $company->settings->country_id;
}
try{
$client->save();
}
catch(\Exception $e) {
nlog("client save failed");
nlog($data);
}
$client->save();
if (! isset($client->number) || empty($client->number) || strlen($client->number) == 0) {
// $client->number = $this->getNextClientNumber($client);
// $client->save();
$x = 1;
@ -111,7 +103,7 @@ class ClientRepository extends BaseRepository
$data['name'] = $client->present()->name();
}
$this->contact_repo->save($data, $client);
$this->contact_repo->save($contact_data, $client);
return $client;
}

View File

@ -44,11 +44,12 @@ class HandleRestore extends AbstractService
return $this->invoice;
}
//determine whether we need to un-delete payments OR just modify the payment amount /applied balances.
//cannot restore an invoice with a deleted payment
foreach ($this->invoice->payments as $payment) {
//restore the payment record
$this->invoice->restore();
if(($this->invoice->paid_to_date == 0) && $payment->is_deleted)
return $this->invoice;
}
//adjust ledger balance
@ -56,8 +57,7 @@ class HandleRestore extends AbstractService
$this->invoice->client
->service()
->updateBalance($this->invoice->balance)
->updatePaidToDate($this->invoice->paid_to_date)
->updateBalanceAndPaidToDate($this->invoice->balance,$this->invoice->paid_to_date)
->save();
$this->windBackInvoiceNumber();
@ -120,11 +120,11 @@ class HandleRestore extends AbstractService
if ($this->adjustment_amount == $this->total_payments) {
$this->invoice->payments()->update(['payments.deleted_at' => null, 'payments.is_deleted' => false]);
} else {
}
//adjust payments down by the amount applied to the invoice payment.
$this->invoice->payments->each(function ($payment) {
$this->invoice->payments->fresh()->each(function ($payment) {
$payment_adjustment = $payment->paymentables
->where('paymentable_type', '=', 'invoices')
->where('paymentable_id', $this->invoice->id)
@ -141,8 +141,7 @@ class HandleRestore extends AbstractService
$payment->restore();
$payment->save();
});
}
return $this;
}

View File

@ -53,16 +53,6 @@ class MarkInvoiceDeleted extends AbstractService
->adjustPaidToDateAndBalance()
->adjustLedger();
$transaction = [
'invoice' => $this->invoice->transaction_event(),
'payment' => $this->invoice->payments()->exists() ? $this->invoice->payments()->first()->transaction_event() : [],
'client' => $this->invoice->client->transaction_event(),
'credit' => [],
'metadata' => ['total_payments' => $this->total_payments, 'balance_adjustment' => $this->balance_adjustment, 'adjustment_amount' => $this->adjustment_amount],
];
// TransactionLog::dispatch(TransactionEvent::INVOICE_DELETED, $transaction, $this->invoice->company->db);
return $this->invoice;
}
@ -87,26 +77,17 @@ class MarkInvoiceDeleted extends AbstractService
return $this;
}
// @deprecated
private function adjustBalance()
{
// $client = $this->invoice->client->fresh();
// $client->balance += $this->balance_adjustment * -1;
// $client->save();
// $this->invoice->client->service()->updateBalance($this->balance_adjustment * -1)->save(); //reduces the client balance by the invoice amount.
return $this;
}
/* Adjust the payment amounts */
private function adjustPayments()
{
//if total payments = adjustment amount - that means we need to delete the payments as well.
if ($this->adjustment_amount == $this->total_payments) {
nlog($this->adjustment_amount);
nlog($this->total_payments);
if ($this->adjustment_amount == $this->total_payments)
$this->invoice->payments()->update(['payments.deleted_at' => now(), 'payments.is_deleted' => true]);
} else {
//adjust payments down by the amount applied to the invoice payment.
@ -125,7 +106,7 @@ class MarkInvoiceDeleted extends AbstractService
$payment->applied -= $payment_adjustment;
$payment->save();
});
}
return $this;
}

View File

@ -30,7 +30,7 @@ trait ClientGroupSettingsSaver
* Saves a setting object.
*
* Works for groups|clients|companies
* @param array $settings The request input settings array
* @param array|object $settings The request input settings array
* @param object $entity The entity which the settings belongs to
* @return void
*/
@ -64,19 +64,6 @@ trait ClientGroupSettingsSaver
$entity_settings->{$key} = $value;
}
$entity->settings = $entity_settings;
try{
$entity->save();
}
catch(\Exception $e){
nlog("client settings failure");
nlog($entity_settings);
nlog($e->getMessage());
}
return $entity_settings;
}

View File

@ -11,12 +11,20 @@
namespace Tests\Feature;
use App\DataMapper\ClientSettings;
use App\Factory\ClientFactory;
use App\Http\Requests\Client\StoreClientRequest;
use App\Models\Client;
use App\Models\Country;
use App\Repositories\ClientContactRepository;
use App\Repositories\ClientRepository;
use App\Utils\Number;
use App\Utils\Traits\ClientGroupSettingsSaver;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\TestCase;
@ -30,6 +38,7 @@ class ClientApiTest extends TestCase
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
use ClientGroupSettingsSaver;
protected function setUp() :void
{
@ -44,6 +53,302 @@ class ClientApiTest extends TestCase
Model::reguard();
}
public function testCsvImportRepositoryPersistance()
{
Client::unguard();
$data = [
'company_id' => $this->company->id,
'name' => 'Christian xx',
'phone' => '',
'address1' => '',
'address2' => '',
'postal_code' => '',
'city' => '',
'state' => '',
'shipping_address1' => '',
'shipping_address2' => '',
'shipping_city' => '',
'shipping_state' => '',
'shipping_postal_code' => '',
'public_notes' => '',
'private_notes' => '',
'website' => '',
'vat_number' => '',
'id_number' => '',
'custom_value1' => '',
'custom_value2' => '',
'custom_value3' => '',
'custom_value4' => '',
'balance' => '0',
'paid_to_date' => '0',
'credit_balance' => 0,
'settings' => [
'entity' => 'App\\Models\\Client',
'currency_id' => '3',
],
'client_hash' => 'xx',
'contacts' =>
[
[
'first_name' => '',
'last_name' => '',
'email' => '',
'phone' => '',
'custom_value1' => '',
'custom_value2' => '',
'custom_value3' => '',
'custom_value4' => '',
]
],
'country_id' => NULL,
'shipping_country_id' => NULL,
'user_id' => $this->user->id,
];
$repository_name = ClientRepository::class;
$factory_name = ClientFactory::class;
$repository = app()->make($repository_name);
$repository->import_mode = true;
$c = $repository->save(array_diff_key($data, ['user_id' => false]), ClientFactory::create($this->company->id, $this->user->id));
Client::reguard();
$c->refresh();
$this->assertEquals("3", $c->settings->currency_id);
}
public function testClientSettingsSave()
{
$std = new \stdClass;
$std->entity = 'App\\Models\\Client';
$std->currency_id = 3;
$this->settings = $this->client->settings;
$this->saveSettings($std, $this->client);
$this->assertTrue(true);
}
public function testClientSettingsSave2()
{
$std = new \stdClass;
$std->entity = 'App\\Models\\Client';
$std->industry_id = '';
$std->size_id = '';
$std->currency_id = 3;
$this->settings = $this->client->settings;
$this->saveSettings($std, $this->client);
$this->assertTrue(true);
}
public function testClientStoreValidation()
{
auth()->login($this->user, false);
auth()->user()->setCompany($this->company);
$data = array (
'company_id' => $this->company->id,
'name' => 'Christian xx',
'phone' => '',
'address1' => '',
'address2' => '',
'postal_code' => '',
'city' => '',
'state' => '',
'shipping_address1' => '',
'shipping_address2' => '',
'shipping_city' => '',
'shipping_state' => '',
'shipping_postal_code' => '',
'public_notes' => '',
'private_notes' => '',
'website' => '',
'vat_number' => '',
'id_number' => '',
'custom_value1' => '',
'custom_value2' => '',
'custom_value3' => '',
'custom_value4' => '',
'balance' => '0',
'paid_to_date' => '0',
'credit_balance' => 0,
'settings' =>
(object) array(
'entity' => 'App\\Models\\Client',
'currency_id' => '3',
),
'client_hash' => 'xx',
'contacts' =>
array (
0 =>
array (
'first_name' => '',
'last_name' => '',
'email' => '',
'phone' => '',
'custom_value1' => '',
'custom_value2' => '',
'custom_value3' => '',
'custom_value4' => '',
),
),
'country_id' => NULL,
'shipping_country_id' => NULL,
'user_id' => $this->user->id,
);
$request_name = StoreClientRequest::class;
$repository_name = ClientRepository::class;
$factory_name = ClientFactory::class;
$repository = app()->make($repository_name);
$repository->import_mode = true;
$_syn_request_class = new $request_name;
$_syn_request_class->setContainer(app());
$_syn_request_class->initialize($data);
$_syn_request_class->prepareForValidation();
$validator = Validator::make($_syn_request_class->all(), $_syn_request_class->rules());
$_syn_request_class->setValidator($validator);
$this->assertFalse($validator->fails());
}
public function testClientImportDataStructure()
{
$data = array (
'company_id' => $this->company->id,
'name' => 'Christian xx',
'phone' => '',
'address1' => '',
'address2' => '',
'postal_code' => '',
'city' => '',
'state' => '',
'shipping_address1' => '',
'shipping_address2' => '',
'shipping_city' => '',
'shipping_state' => '',
'shipping_postal_code' => '',
'public_notes' => '',
'private_notes' => '',
'website' => '',
'vat_number' => '',
'id_number' => '',
'custom_value1' => '',
'custom_value2' => '',
'custom_value3' => '',
'custom_value4' => '',
'balance' => '0',
'paid_to_date' => '0',
'credit_balance' => 0,
'settings' =>
(object) array(
'entity' => 'App\\Models\\Client',
'currency_id' => '3',
),
'client_hash' => 'xx',
'contacts' =>
array (
0 =>
array (
'first_name' => '',
'last_name' => '',
'email' => '',
'phone' => '',
'custom_value1' => '',
'custom_value2' => '',
'custom_value3' => '',
'custom_value4' => '',
),
),
'country_id' => NULL,
'shipping_country_id' => NULL,
'user_id' => $this->user->id,
);
$crepo = new ClientRepository(new ClientContactRepository());
$c = $crepo->save(array_diff_key($data, ['user_id' => false]), ClientFactory::create($this->company->id, $this->user->id));
$c->saveQuietly();
$this->assertEquals('Christian xx', $c->name);
$this->assertEquals('3', $c->settings->currency_id);
}
public function testClientCsvImport()
{
$settings = ClientSettings::defaults();
$settings->currency_id = "840";
$data = [
'name' => $this->faker->firstName(),
'id_number' => 'Coolio',
'settings' => (array)$settings,
'contacts' => [
[
'first_name' => '',
'last_name' => '',
'email' => '',
'phone' => '',
'custom_value1' => '',
'custom_value2' => '',
'custom_value3' => '',
'custom_value4' => '',
]
]
];
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(200);
$crepo = new ClientRepository(new ClientContactRepository());
$c = $crepo->save($data, ClientFactory::create($this->company->id, $this->user->id));
$c->saveQuietly();
}
public function testIllegalPropertiesInClientSettings()
{
$settings = [

View File

@ -162,6 +162,7 @@ class DeleteInvoiceTest extends TestCase
$payment = $payment->fresh();
$this->assertTrue($payment->is_deleted);
$this->assertEquals(0, $payment->amount);
$this->assertEquals(4, $payment->status_id);
$client->fresh();