* Version bump

* Refactors for refunds / credits

* Working on Company Ledger

* Company Ledger OpenAPI Documentation

* Version Bump

* Fixes for internal composer update
This commit is contained in:
David Bomba 2020-04-11 21:19:05 +10:00 committed by GitHub
parent 4c0bba7814
commit ba55cc32e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 310 additions and 48 deletions

View File

@ -1 +1 @@
0.0.3 5.0.4

View File

@ -2,9 +2,12 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Composer\Composer;
use Composer\Factory;
use Composer\IO\NullIO;
use Composer\Installer;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Composer\Console\Application;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
class ArtisanUpgrade extends Command class ArtisanUpgrade extends Command
@ -54,10 +57,21 @@ class ArtisanUpgrade extends Command
\Log::error("I wasn't able to optimize."); \Log::error("I wasn't able to optimize.");
} }
putenv('COMPOSER_HOME=' . __DIR__ . '/vendor/bin/composer'); $composer = Factory::create(new NullIO(), base_path('composer.json'), false);
$input = new ArrayInput(array('command' => 'update'));
$application = new Application(); $output = Installer::create(new NullIO, $composer)
$application->setAutoExit(true); // prevent `$application->run` method from exitting the script ->setVerbose()
$application->run($input); ->setUpdate(true)
->run();
\Log::error(print_r($output,1));
// putenv('COMPOSER_HOME=' . __DIR__ . '/vendor/bin/composer');
// $input = new ArrayInput(array('command' => 'update'));
// $application = new Application();
// $application->setAutoExit(true); // prevent `$application->run` method from exitting the script
// $application->run($input);
} }
} }

View File

@ -473,22 +473,24 @@ class CreateTestData extends Command
$invoice->service()->createInvitations(); $invoice->service()->createInvitations();
if (rand(0, 1)) { if (rand(0, 1)) {
$payment = PaymentFactory::create($client->company->id, $client->user->id); // $payment = PaymentFactory::create($client->company->id, $client->user->id);
$payment->date = $dateable; // $payment->date = $dateable;
$payment->client_id = $client->id; // $payment->client_id = $client->id;
$payment->amount = $invoice->balance; // $payment->amount = $invoice->balance;
$payment->transaction_reference = rand(0, 500); // $payment->transaction_reference = rand(0, 500);
$payment->type_id = PaymentType::CREDIT_CARD_OTHER; // $payment->type_id = PaymentType::CREDIT_CARD_OTHER;
$payment->status_id = Payment::STATUS_COMPLETED; // $payment->status_id = Payment::STATUS_COMPLETED;
$payment->number = $client->getNextPaymentNumber($client); // $payment->number = $client->getNextPaymentNumber($client);
$payment->currency_id = 1; // $payment->currency_id = 1;
$payment->save(); // $payment->save();
$payment->invoices()->save($invoice); // $payment->invoices()->save($invoice);
event(new PaymentWasCreated($payment, $payment->company)); $invoice = $invoice->service()->markPaid()->save();
$payment->service()->updateInvoicePayment(); //$payment = $invoice->payments->first();
//$payment->service()->updateInvoicePayment();
//UpdateInvoicePayment::dispatchNow($payment, $payment->company); //UpdateInvoicePayment::dispatchNow($payment, $payment->company);
} }
//@todo this slow things down, but gives us PDFs of the invoices for inspection whilst debugging. //@todo this slow things down, but gives us PDFs of the invoices for inspection whilst debugging.

View File

@ -0,0 +1,77 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use App\Http\Requests\CompanyLedger\ShowCompanyLedgerRequest;
use App\Models\CompanyLedger;
use App\Transformers\CompanyLedgerTransformer;
use Illuminate\Http\Request;
class CompanyLedgerController extends BaseController
{
protected $entity_type = CompanyLedger::class;
protected $entity_transformer = CompanyLedgerTransformer::class;
public function __construct()
{
parent::__construct();
}
/**
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
*
* @OA\Get(
* path="/api/v1/company_ledger",
* operationId="getCompanyLedger",
* tags={"company_ledger"},
* summary="Gets a list of company_ledger",
* description="Lists the company_ledger.",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="A list of company_ledger",
* @OA\Header(header="X-API-Version", ref="#/components/headers/X-API-Version"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/CompanyLedger"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*
*/
public function index(ShowCompanyLedgerRequest $request)
{
$company_ledger = CompanyLedger::whereCompanyId(auth()->user()->company()->id)->orderBy('id', 'ASC');
return $this->listResponse($company_ledger);
}
}

View File

@ -0,0 +1,13 @@
<?php
/**
* @OA\Schema(
* schema="CompanyLedger",
* type="object",
* @OA\Property(property="entity_id", type="string", example="AS3df3A", description="This field will reference one of the following entity hashed ID payment_id, invoice_id or credit_id"),
* @OA\Property(property="notes", type="string", example="Credit note for invoice #3212", description="The notes which reference this entry of the ledger"),
* @OA\Property(property="balance", type="number", format="float", example="10.00", description="The client balance"),
* @OA\Property(property="adjustment", type="number", format="float", example="10.00", description="The amount the client balance is adjusted by"),
* @OA\Property(property="updated_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* @OA\Property(property="created_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* )
*/

View File

@ -0,0 +1,30 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\CompanyLedger;
use App\Http\Requests\Request;
class ShowCompanyLedgerRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
}

View File

@ -67,7 +67,7 @@ class RefundPaymentRequest extends Request
$input = $this->all(); $input = $this->all();
$rules = [ $rules = [
'id' => 'required', 'id' => 'bail|required',
'id' => new ValidRefundableRequest($input), 'id' => new ValidRefundableRequest($input),
'amount' => 'numeric', 'amount' => 'numeric',
'date' => 'required', 'date' => 'required',

View File

@ -44,6 +44,11 @@ class ValidRefundableRequest implements Rule
public function passes($attribute, $value) public function passes($attribute, $value)
{ {
if(!array_key_exists('id', $this->input)){
$this->error_msg = "Payment `id` required.";
return false;
}
$payment = Payment::whereId($this->input['id'])->first(); $payment = Payment::whereId($this->input['id'])->first();
if (!$payment) { if (!$payment) {

View File

@ -43,6 +43,12 @@ class ValidRefundableInvoices implements Rule
public function passes($attribute, $value) public function passes($attribute, $value)
{ {
if(!array_key_exists('id', $this->input)){
$this->error_msg = "Payment `id` required.";
return false;
}
$payment = Payment::whereId($this->input['id'])->first(); $payment = Payment::whereId($this->input['id'])->first();
if (!$payment) { if (!$payment) {
@ -50,10 +56,11 @@ class ValidRefundableInvoices implements Rule
return false; return false;
} }
if (request()->has('amount') && (request()->input('amount') > ($payment->amount - $payment->refunded))) { /*We are not sending the Refunded amount in the 'amount field, this is the Payment->amount, need to skip this check. */
$this->error_msg = "Attempting to refunded more than payment amount, enter a value equal to or lower than the payment amount of ". $payment->amount; // if (request()->has('amount') && (request()->input('amount') > ($payment->amount - $payment->refunded))) {
return false; // $this->error_msg = "Attempting to refund more than payment amount, enter a value equal to or lower than the payment amount of ". $payment->amount;
} // return false;
// }
/*If no invoices has been sent, then we apply the payment to the client account*/ /*If no invoices has been sent, then we apply the payment to the client account*/
$invoices = []; $invoices = [];
@ -65,6 +72,7 @@ class ValidRefundableInvoices implements Rule
} }
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
if (! $invoice->isRefundable()) { if (! $invoice->isRefundable()) {
$this->error_msg = "Invoice id ".$invoice->hashed_id ." cannot be refunded"; $this->error_msg = "Invoice id ".$invoice->hashed_id ." cannot be refunded";
return false; return false;

View File

@ -87,7 +87,7 @@ class CreateCreditPdf implements ShouldQueue
//todo - move this to the client creation stage so we don't keep hitting this unnecessarily //todo - move this to the client creation stage so we don't keep hitting this unnecessarily
Storage::makeDirectory($path, 0755); Storage::makeDirectory($path, 0755);
$pdf = $this->makePdf(null, null, $html); $pdf = $this->makePdf(null, null, $html);
$instance = Storage::disk($this->disk)->put($file_path, $pdf); $instance = Storage::disk($this->disk)->put($file_path, $pdf);

View File

@ -83,16 +83,18 @@ class CreateQuotePdf implements ShouldQueue
//todo - move this to the client creation stage so we don't keep hitting this unnecessarily //todo - move this to the client creation stage so we don't keep hitting this unnecessarily
Storage::makeDirectory($path, 0755); Storage::makeDirectory($path, 0755);
$all_pages_header = $settings->all_pages_header;
$all_pages_footer = $settings->all_pages_footer;
$quote_number = $this->quote->number; $quote_number = $this->quote->number;
$design_body = $designer->build()->getHtml(); $design_body = $designer->build()->getHtml();
$html = $this->generateEntityHtml($designer, $this->quote, $this->contact); $html = $this->generateEntityHtml($designer, $this->quote, $this->contact);
$pdf = $this->makePdf($all_pages_header, $all_pages_footer, $html); //$start = microtime(true);
$pdf = $this->makePdf(null, null, $html);
//\Log::error("PDF Build time = ". (microtime(true) - $start));
$file_path = $path . $quote_number . '.pdf'; $file_path = $path . $quote_number . '.pdf';
$instance = Storage::disk($this->disk)->put($file_path, $pdf); $instance = Storage::disk($this->disk)->put($file_path, $pdf);

View File

@ -114,6 +114,11 @@ class Client extends BaseModel implements HasLocalePreference
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
]; ];
public function ledger()
{
return $this->hasMany(CompanyLedger::class);
}
public function gateway_tokens() public function gateway_tokens()
{ {
return $this->hasMany(ClientGatewayToken::class); return $this->hasMany(ClientGatewayToken::class);

View File

@ -133,6 +133,11 @@ class Company extends BaseModel
self::ENTITY_RECURRING_QUOTE => 2048, self::ENTITY_RECURRING_QUOTE => 2048,
]; ];
public function ledger()
{
return $this->hasMany(CompanyLedger::class);
}
public function getCompanyIdAttribute() public function getCompanyIdAttribute()
{ {
return $this->encodePrimaryKey($this->id); return $this->encodePrimaryKey($this->id);

View File

@ -56,7 +56,6 @@ class LedgerService
$balance = $company_ledger->balance; $balance = $company_ledger->balance;
} }
$company_ledger = CompanyLedgerFactory::create($this->entity->company_id, $this->entity->user_id); $company_ledger = CompanyLedgerFactory::create($this->entity->company_id, $this->entity->user_id);
$company_ledger->client_id = $this->entity->client_id; $company_ledger->client_id = $this->entity->client_id;
$company_ledger->adjustment = $adjustment; $company_ledger->adjustment = $adjustment;
@ -66,6 +65,29 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger); $this->entity->company_ledger()->save($company_ledger);
} }
public function updateCreditBalance($adjustment, $notes = '')
{
$balance = 0;
$company_ledger = $this->ledger();
if ($company_ledger) {
$balance = $company_ledger->balance;
}
$company_ledger = CompanyLedgerFactory::create($this->entity->company_id, $this->entity->user_id);
$company_ledger->client_id = $this->entity->client_id;
$company_ledger->adjustment = $adjustment;
$company_ledger->notes = $notes;
$company_ledger->balance = $balance + $adjustment;
$company_ledger->save();
$this->entity->company_ledger()->save($company_ledger);
return $this;
}
private function ledger() :?CompanyLedger private function ledger() :?CompanyLedger
{ {
return CompanyLedger::whereClientId($this->entity->client_id) return CompanyLedger::whereClientId($this->entity->client_id)

View File

@ -10,6 +10,9 @@ use App\Models\SystemLog;
class UpdateInvoicePayment class UpdateInvoicePayment
{ {
/**
* @deprecated This is bad logic, assumes too much.
*/
public $payment; public $payment;
public function __construct($payment) public function __construct($payment)

View File

@ -15,8 +15,10 @@ use App\Models\Activity;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\CompanyLedger;
use App\Transformers\ActivityTransformer; use App\Transformers\ActivityTransformer;
use App\Transformers\ClientGatewayTokenTransformer; use App\Transformers\ClientGatewayTokenTransformer;
use App\Transformers\CompanyLedgerTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
/** /**
@ -36,6 +38,7 @@ class ClientTransformer extends EntityTransformer
protected $availableIncludes = [ protected $availableIncludes = [
'gateway_tokens', 'gateway_tokens',
'activities', 'activities',
'ledger',
]; ];
@ -69,6 +72,13 @@ class ClientTransformer extends EntityTransformer
return $this->includeCollection($client->gateway_tokens, $transformer, ClientGatewayToken::class); return $this->includeCollection($client->gateway_tokens, $transformer, ClientGatewayToken::class);
} }
public function includeLedger(Client $client)
{
$transformer = new CompanyLedgerTransformer($this->serializer);
return $this->includeCollection($client->ledger, $transformer, CompanyLedger::class);
}
/** /**
* @param Client $client * @param Client $client
* *

View File

@ -0,0 +1,43 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Transformers;
use App\Models\CompanyLedger;
use App\Utils\Traits\MakesHash;
/**
* Class CompanyLedgerTransformer.
*
*/
class CompanyLedgerTransformer extends EntityTransformer
{
use MakesHash;
/**
* @param ClientContact $company_ledger
*
* @return array
*
*/
public function transform(CompanyLedger $company_ledger)
{
$entity_name = lcfirst(class_basename($company_ledger->company_ledgerable_type)) . '_id';
return [
$entity_name => (string)$this->encodePrimaryKey($company_ledger->company_ledgerable_id),
'notes' => (string)$company_ledger->notes ?: '',
'balance' => (float) $company_ledger->balance,
'adjustment' => (float) $company_ledger->adjustment,
'created_at' => (int)$company_ledger->created_at,
'updated_at' => (int)$company_ledger->updated_at,
'archived_at' => (int)$company_ledger->deleted_at,
];
}
}

View File

@ -16,6 +16,7 @@ use App\Models\Activity;
use App\Models\Client; use App\Models\Client;
use App\Models\Company; use App\Models\Company;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\CompanyLedger;
use App\Models\CompanyUser; use App\Models\CompanyUser;
use App\Models\Design; use App\Models\Design;
use App\Models\Expense; use App\Models\Expense;
@ -27,6 +28,7 @@ use App\Models\Quote;
use App\Models\Task; use App\Models\Task;
use App\Models\TaxRate; use App\Models\TaxRate;
use App\Models\User; use App\Models\User;
use App\Transformers\CompanyLedgerTransformer;
use App\Transformers\TaskTransformer; use App\Transformers\TaskTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -68,6 +70,7 @@ class CompanyTransformer extends EntityTransformer
'quotes', 'quotes',
'projects', 'projects',
'tasks', 'tasks',
'ledger',
]; ];
@ -233,4 +236,11 @@ class CompanyTransformer extends EntityTransformer
return $this->includeCollection($company->designs()->get(), $transformer, Design::class); return $this->includeCollection($company->designs()->get(), $transformer, Design::class);
} }
public function includeLedger(Company $company)
{
$transformer = new CompanyLedgerTransformer($this->serializer);
return $this->includeCollection($company->ledger, $transformer, CompanyLedger::class);
}
} }

View File

@ -100,6 +100,8 @@ trait Refundable
$line_items = []; $line_items = [];
$ledger_string = '';
foreach ($data['invoices'] as $invoice) { foreach ($data['invoices'] as $invoice) {
$inv = Invoice::find($invoice['invoice_id']); $inv = Invoice::find($invoice['invoice_id']);
@ -111,6 +113,8 @@ trait Refundable
$credit_line_item->line_total = $invoice['amount']; $credit_line_item->line_total = $invoice['amount'];
$credit_line_item->date = $data['date']; $credit_line_item->date = $data['date'];
$ledger_string .= $credit_line_item->notes . ' ';
$line_items[] = $credit_line_item; $line_items[] = $credit_line_item;
} }
@ -171,7 +175,9 @@ trait Refundable
$this->save(); $this->save();
$this->adjustInvoices($data); $client_balance_adjustment = $this->adjustInvoices($data);
$credit_note->ledger()->updateCreditBalance($client_balance_adjustment, $ledger_string);
$this->client->paid_to_date -= $data['amount']; $this->client->paid_to_date -= $data['amount'];
$this->client->save(); $this->client->save();
@ -216,8 +222,10 @@ trait Refundable
return $credit_note; return $credit_note;
} }
private function adjustInvoices(array $data) :void private function adjustInvoices(array $data)
{ {
$adjustment_amount = 0;
foreach ($data['invoices'] as $refunded_invoice) { foreach ($data['invoices'] as $refunded_invoice) {
$invoice = Invoice::find($refunded_invoice['invoice_id']); $invoice = Invoice::find($refunded_invoice['invoice_id']);
@ -231,12 +239,14 @@ trait Refundable
$client = $invoice->client; $client = $invoice->client;
$adjustment_amount += $refunded_invoice['amount'];
$client->balance += $refunded_invoice['amount']; $client->balance += $refunded_invoice['amount'];
///$client->paid_to_date -= $refunded_invoice['amount'];
$client->save(); $client->save();
//todo adjust ledger balance here? or after and reference the credit and its total //todo adjust ledger balance here? or after and reference the credit and its total
} }
return $adjustment_amount;
} }
} }

View File

@ -11,8 +11,8 @@ return [
'app_env' => env('APP_ENV', 'local'), 'app_env' => env('APP_ENV', 'local'),
'app_url' => env('APP_URL', ''), 'app_url' => env('APP_URL', ''),
'app_domain' => env('APP_DOMAIN', ''), 'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '0.0.3', 'app_version' => '5.0.4',
'api_version' => '0.0.3', 'api_version' => '5.0.4',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),
'google_maps_api_key' => env('GOOGLE_MAPS_API_KEY'), 'google_maps_api_key' => env('GOOGLE_MAPS_API_KEY'),

View File

@ -12,17 +12,17 @@ $factory->define(App\Models\Company::class, function (Faker $faker) {
'settings' => CompanySettings::defaults(), 'settings' => CompanySettings::defaults(),
'custom_fields' => (object) [ 'custom_fields' => (object) [
'invoice1' => '1|date', 'invoice1' => '1|date',
'invoice2' => '2|switch', // 'invoice2' => '2|switch',
'invoice3' => '3|', // 'invoice3' => '3|',
'invoice4' => '4', // 'invoice4' => '4',
'client1'=>'1', // 'client1'=>'1',
'client2'=>'2', // 'client2'=>'2',
'client3'=>'3|date', // 'client3'=>'3|date',
'client4'=>'4|switch', // 'client4'=>'4|switch',
'company1'=>'1|date', // 'company1'=>'1|date',
'company2'=>'2|switch', // 'company2'=>'2|switch',
'company3'=>'3', // 'company3'=>'3',
'company4'=>'4', // 'company4'=>'4',
], ],
]; ];
}); });

View File

@ -6,7 +6,7 @@
convertErrorsToExceptions="true" convertErrorsToExceptions="true"
convertNoticesToExceptions="true" convertNoticesToExceptions="true"
convertWarningsToExceptions="true" convertWarningsToExceptions="true"
processIsolation="false" processIsolation="true"
stopOnFailure="true"> stopOnFailure="true">
<testsuites> <testsuites>
<testsuite name="Unit"> <testsuite name="Unit">

View File

@ -124,6 +124,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::resource('subscriptions', 'SubscriptionController'); Route::resource('subscriptions', 'SubscriptionController');
Route::post('subscriptions/bulk', 'SubscriptionController@bulk')->name('subscriptions.bulk'); Route::post('subscriptions/bulk', 'SubscriptionController@bulk')->name('subscriptions.bulk');
/*Company Ledger */
Route::get('company_ledger', 'CompanyLedgerController@index')->name('company_ledger.index');
/* /*
Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit