This commit is contained in:
= 2021-08-21 20:38:35 +10:00
commit 6e951a1739
653 changed files with 1446547 additions and 515228 deletions

View File

@ -5,7 +5,7 @@ APP_DEBUG=true
APP_URL=http://ninja.test APP_URL=http://ninja.test
MULTI_DB_ENABLED=false MULTI_DB_ENABLED=false
# database # database
DB_CONNECTION=db-ninja-01 DB_CONNECTION=mysql
DB_DATABASE1=ninja DB_DATABASE1=ninja
DB_USERNAME1=root DB_USERNAME1=root
DB_PASSWORD1=ninja DB_PASSWORD1=ninja

View File

@ -5,20 +5,14 @@ APP_DEBUG=false
APP_URL=http://localhost APP_URL=http://localhost
DB_CONNECTION=db-ninja-01 DB_CONNECTION=mysql
MULTI_DB_ENABLED=false MULTI_DB_ENABLED=false
DB_HOST1=localhost DB_HOST=localhost
DB_DATABASE1=ninja DB_DATABASE=ninja
DB_USERNAME1=ninja DB_USERNAME=ninja
DB_PASSWORD1=ninja DB_PASSWORD=ninja
DB_PORT1=3306 DB_PORT=3306
DB_HOST2=localhost
DB_DATABASE2=ninja2
DB_USERNAME2=ninja
DB_PASSWORD2=ninja
DB_PORT2=3306
DEMO_MODE=false DEMO_MODE=false

View File

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
operating-system: ['ubuntu-18.04', 'ubuntu-20.04'] operating-system: ['ubuntu-18.04', 'ubuntu-20.04']
php-versions: ['7.3','7.4','8.0'] php-versions: ['7.4','8.0']
phpunit-versions: ['latest'] phpunit-versions: ['latest']
env: env:
@ -49,6 +49,10 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps: steps:
- name: Add hosts to /etc/hosts
run: |
sudo echo "127.0.0.1 ninja.test" | sudo tee -a /etc/hosts
- name: Start mysql service - name: Start mysql service
run: | run: |
sudo /etc/init.d/mysql start sudo /etc/init.d/mysql start

View File

@ -49,37 +49,16 @@ jobs:
sudo rm -rf bootstrap/cache/* sudo rm -rf bootstrap/cache/*
sudo rm -rf node_modules sudo rm -rf node_modules
sudo rm -rf .git sudo rm -rf .git
# - name: Prune Git History
# run: | - name: Build project
# sudo git gc
# sudo git gc --aggressive
# sudo git prune
- name: Build project # This would actually build your project, using zip for an example artifact
run: | run: |
zip -r ./invoiceninja.zip .* -x "../*" zip -r ./invoiceninja.zip .* -x "../*"
- name: Get tag name - name: Release
id: get_tag_name uses: softprops/action-gh-release@v1
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}-release if: startsWith(github.ref, 'refs/tags/')
- name: Create Release
id: create_release
uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ steps.get_tag_name.outputs.VERSION }} files: |
release_name: Release ${{ steps.get_tag_name.outputs.VERSION }} invoiceninja.zip
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{secrets.RELEASE_TOKEN}}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ./invoiceninja.zip
asset_name: invoiceninja.zip
asset_content_type: application/zip

View File

@ -1,64 +0,0 @@
# Release notes
## [Unreleased (daily channel)](https://github.com/invoiceninja/invoiceninja/tree/v5-develop)
## [v5.2.0-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.2.0-release)
## Added:
- Timezone Offset: Schedule emails based on timezone and time offsets.
- Force client country to system country if none is set.
- GMail Oauth via web
## Fixed:
- Add Cache-control: no-cache to prevent overaggressive caching of assets
- Improved labelling in the settings (client portal)
- Client portal: Multiple accounts access improvements (#5703)
- Client portal: "Credits" updates (#5734)
- Client portal: Make sidebar white color, in order to make logo displaying more simple. (#5753)
- Inject small delay into emails to allow all resources to be produced (ie PDFs) prior to sending
- Fixes for endless reminders not firing
## [v5.1.56-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.56-release)
## Fixed:
- Fix User created/updated/deleted Actvity display format
- Fix for Stripe autobill / token regression
## Added:
- Invoice / Quote / Credit created notifications
- Logout route - deletes all auth tokens
## [v5.1.54-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.54-release)
## Fixed:
- Fixes for e-mails, encoding & parsing invalid HTML
## [v5.1.50-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.50-release)
## Fixed:
- Refactor of e-mail templates
- Client portal: Invoices & recurring invoices are now sorted by date (by default)
## Added:
- Public notes of entities will now show in #footer section of designs (previously totals table).
## [v5.1.47-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.47-release)
### Added:
- Subscriptions are now going to show the frequency in the table (#5412)
- Subscriptions: During upgrade webhook request message will be shown for easier debugging (#5411)
- PDF: Custom fields now will be shared across invoices, quotes & credits (#5410)
- Client portal: Invoices are now sorted in the descending order (#5408)
- Payments: ACH notification after the initial bank account connecting process (#5405)
### Fixed:
- Fixes for counters where patterns without {$counter} could causes endless recursion.
- Fixes for surcharge tax displayed amount on PDF.
- Fixes for custom designs not rendering the custom template
- Fixes for missing bulk actions on Subscriptions
- Fixes CSS padding on the show page for recurring invoices (#5412)
- Fixes for rendering invalid HTML & parsing invalid XML (#5395)
### Removed:
- Removed one-time payments table (#5412)
## v5.1.43
### Fixed:
- Whitelabel regression.

View File

@ -4,15 +4,35 @@
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop) ![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable) ![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/d16c78aad8574466bf83232b513ef4fb)](https://www.codacy.com/gh/turbo124/invoiceninja/dashboard?utm_source=github.com&utm_medium=referral&utm_content=turbo124/invoiceninja&utm_campaign=Badge_Grade)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d39acb4bf0f74a0698dc77f382769ba5)](https://www.codacy.com/app/turbo124/invoiceninja?utm_source=github.com&utm_medium=referral&utm_content=invoiceninja/invoiceninja&utm_campaign=Badge_Grade) # Invoice Ninja 5
# Invoice Ninja version 5! ## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org)
### We're on Slack, join us at [slack.invoiceninja.com](http://slack.invoiceninja.com), [forum.invoiceninja.com](https://forum.invoiceninja.com) or if you like [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/)
Just make sure to add the `invoice-ninja` tag to your question.
## Introduction
Version 5 of Invoice Ninja is here! We've taken the best parts of version 4 and bolted on all of the most requested features to produce a invoicing application like no other.
The new interface has a lot more functionality so it isn't a carbon copy of v4, but once you get used to the new layout and functionality we are sure you will love it!
## Referral Program
* Earn 50% of Pro & Enterprise Plans up to 4 years - [Learn more](https://www.invoiceninja.com/referral-program/)
## Recommended Providers
* [Stripe](https://stripe.com/)
* [Postmark](https://postmarkapp.com/)
## Development
* [API Documentation](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
* [APP Documentation](https://invoiceninja.github.io/)
## Quick Start ## Quick Start
Currently the client portal and API are of alpha quality, to get started:
```bash ```bash
git clone https://github.com/invoiceninja/invoiceninja.git git clone https://github.com/invoiceninja/invoiceninja.git
git checkout v5-stable git checkout v5-stable
@ -69,6 +89,8 @@ To improve chances of PRs being merged please include tests to ensure your code
API documentation is hosted using Swagger and can be found [HERE](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja) API documentation is hosted using Swagger and can be found [HERE](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
Installation, Configuration and Troubleshooting documentation can be found [HERE] (https://invoiceninja.github.io)
## Credits ## Credits
* [Hillel Coren](https://hillelcoren.com/) * [Hillel Coren](https://hillelcoren.com/)
* [David Bomba](https://github.com/turbo124) * [David Bomba](https://github.com/turbo124)
@ -83,12 +105,10 @@ API documentation is hosted using Swagger and can be found [HERE](https://app.sw
* [Clemens Mol](https://github.com/clemensmol) * [Clemens Mol](https://github.com/clemensmol)
* [Benjamin Beganović](https://github.com/beganovich) * [Benjamin Beganović](https://github.com/beganovich)
## Current work in progress ## Security
Invoice Ninja is currently being written in a combination of Laravel for the API and Client Portal and Flutter for the front end management console. This will allow an immersive and consistent experience across any device: mobile, tablet or desktop. If you find a security issue with this application please send an email to contact@invoiceninja.com Please follow responsible disclosure procedures if you detect an issue. For further information on responsible disclosure please read [here](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html)
To manage our workflow we will be creating separate branches for the client (Flutter) and server (Laravel API / Client Portal) and merge these into a release branch for deployments.
## License ## License
Invoice Ninja is released under the Attribution Assurance License. Invoice Ninja is released under the Elastic License.
See [LICENSE](LICENSE) for details. See [LICENSE](LICENSE) for details.

View File

@ -1 +1 @@
5.2.5 5.3.0

View File

@ -16,19 +16,21 @@ use App\Factory\ClientContactFactory;
use App\Models\Account; use App\Models\Account;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyLedger; use App\Models\CompanyLedger;
use App\Models\Contact; use App\Models\Contact;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Paymentable;
use App\Utils\Ninja; use App\Utils\Ninja;
use DB; use DB;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Mail; use Mail;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Illuminate\Support\Str;
/* /*
@ -67,7 +69,7 @@ class CheckData extends Command
/** /**
* @var string * @var string
*/ */
protected $signature = 'ninja:check-data {--database=} {--fix=} {--client_id=}'; protected $signature = 'ninja:check-data {--database=} {--fix=} {--client_id=} {--paid_to_date=} {--client_balance=}';
/** /**
* @var string * @var string
@ -78,8 +80,14 @@ class CheckData extends Command
protected $isValid = true; protected $isValid = true;
protected $wrong_paid_to_dates = 0;
protected $wrong_balances = 0;
public function handle() public function handle()
{ {
$time_start = microtime(true);
$database_connection = $this->option('database') ? $this->option('database') : 'Connected to Default DB'; $database_connection = $this->option('database') ? $this->option('database') : 'Connected to Default DB';
$fix_status = $this->option('fix') ? "Fixing Issues" : "Just checking issues "; $fix_status = $this->option('fix') ? "Fixing Issues" : "Just checking issues ";
@ -92,6 +100,7 @@ class CheckData extends Command
$this->checkInvoiceBalances(); $this->checkInvoiceBalances();
$this->checkInvoicePayments(); $this->checkInvoicePayments();
$this->checkPaidToDates(); $this->checkPaidToDates();
// $this->checkPaidToCompanyDates();
$this->checkClientBalances(); $this->checkClientBalances();
$this->checkContacts(); $this->checkContacts();
$this->checkCompanyData(); $this->checkCompanyData();
@ -103,6 +112,8 @@ class CheckData extends Command
} }
$this->logMessage('Done: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE)); $this->logMessage('Done: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE));
$this->logMessage('Total execution time in seconds: ' . (microtime(true) - $time_start));
$errorEmail = config('ninja.error_email'); $errorEmail = config('ninja.error_email');
if ($errorEmail) { if ($errorEmail) {
@ -223,38 +234,38 @@ class CheckData extends Command
} }
} }
// check for more than one primary contact // // check for more than one primary contact
$clients = DB::table('clients') // $clients = DB::table('clients')
->leftJoin('client_contacts', function ($join) { // ->leftJoin('client_contacts', function ($join) {
$join->on('client_contacts.client_id', '=', 'clients.id') // $join->on('client_contacts.client_id', '=', 'clients.id')
->where('client_contacts.is_primary', '=', true) // ->where('client_contacts.is_primary', '=', true)
->whereNull('client_contacts.deleted_at'); // ->whereNull('client_contacts.deleted_at');
}) // })
->groupBy('clients.id') // ->groupBy('clients.id')
->havingRaw('count(client_contacts.id) != 1'); // ->havingRaw('count(client_contacts.id) != 1');
if ($this->option('client_id')) { // if ($this->option('client_id')) {
$clients->where('clients.id', '=', $this->option('client_id')); // $clients->where('clients.id', '=', $this->option('client_id'));
} // }
$clients = $clients->get(['clients.id', 'clients.user_id', 'clients.company_id']); // $clients = $clients->get(['clients.id', 'clients.user_id', 'clients.company_id']);
$this->logMessage($clients->count().' clients without a single primary contact'); // // $this->logMessage($clients->count().' clients without a single primary contact');
if ($this->option('fix') == 'true') { // // if ($this->option('fix') == 'true') {
foreach ($clients as $client) { // // foreach ($clients as $client) {
$this->logMessage("Fixing missing primary contacts #{$client->id}"); // // $this->logMessage("Fixing missing primary contacts #{$client->id}");
$new_contact = ClientContactFactory::create($client->company_id, $client->user_id); // // $new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
$new_contact->client_id = $client->id; // // $new_contact->client_id = $client->id;
$new_contact->contact_key = Str::random(40); // // $new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true; // // $new_contact->is_primary = true;
$new_contact->save(); // // $new_contact->save();
} // // }
} // // }
if ($clients->count() > 0) { // if ($clients->count() > 0) {
$this->isValid = false; // $this->isValid = false;
} // }
} }
private function checkFailedJobs() private function checkFailedJobs()
@ -303,63 +314,135 @@ class CheckData extends Command
} }
} }
// private function checkPaidToCompanyDates()
// {
// Company::cursor()->each(function ($company){
// $payments = Payment::where('is_deleted', 0)
// ->where('company_id', $company->id)
// ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED])
// ->pluck('id');
// $unapplied = Payment::where('is_deleted', 0)
// ->where('company_id', $company->id)
// ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
// ->sum(\DB::Raw('amount - applied'));
// $paymentables = Paymentable::whereIn('payment_id', $payments)->sum(\DB::Raw('amount - refunded'));
// $client_paid_to_date = Client::where('company_id', $company->id)->where('is_deleted', 0)->withTrashed()->sum('paid_to_date');
// $total_payments = $paymentables + $unapplied;
// if (round($total_payments, 2) != round($client_paid_to_date, 2)) {
// $this->wrong_paid_to_dates++;
// $this->logMessage($company->present()->name.' id = # '.$company->id." - Paid to date does not match Client Paid To Date = {$client_paid_to_date} - Invoice Payments = {$total_payments}");
// }
// });
// }
private function checkPaidToDates() private function checkPaidToDates()
{ {
$wrong_paid_to_dates = 0; $this->wrong_paid_to_dates = 0;
$credit_total_applied = 0; $credit_total_applied = 0;
Client::withTrashed()->where('is_deleted', 0)->cursor()->each(function ($client) use ($wrong_paid_to_dates, $credit_total_applied) {
$clients = DB::table('clients')
->leftJoin('payments', function($join) {
$join->on('payments.client_id', '=', 'clients.id')
->where('payments.is_deleted', 0)
->whereIn('payments.status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]);
})
->where('clients.is_deleted',0)
->where('clients.updated_at', '>', now()->subDays(2))
->groupBy('clients.id')
->havingRaw('clients.paid_to_date != sum(coalesce(payments.amount - payments.refunded, 0))')
->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(coalesce(payments.amount - payments.refunded, 0)) as amount')]);
/* Due to accounting differences we need to perform a second loop here to ensure there actually is an issue */
$clients->each(function ($client_record) use ($credit_total_applied) {
$client = Client::withTrashed()->find($client_record->id);
$total_invoice_payments = 0; $total_invoice_payments = 0;
foreach ($client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get() as $invoice) { foreach ($client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get() as $invoice) {
$total_amount = $invoice->payments()->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->get()->sum('pivot.amount'); $total_invoice_payments += $invoice->payments()
$total_refund = $invoice->payments()->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->get()->sum('pivot.refunded'); ->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->selectRaw('sum(paymentables.amount - paymentables.refunded) as p')
->pluck('p')
->first();
$total_invoice_payments += ($total_amount - $total_refund);
} }
//commented IN 27/06/2021 - sums ALL client payments AND the unapplied amounts to match the client paid to date
$p = Payment::where('client_id', $client->id)
->where('is_deleted', 0)
->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->sum(DB::Raw('amount - applied'));
$total_invoice_payments += $p;
// 10/02/21 // 10/02/21
foreach ($client->payments as $payment) { foreach ($client->payments as $payment) {
$credit_total_applied += $payment->paymentables()->where('paymentable_type', App\Models\Credit::class)->get()->sum(DB::raw('amount'));
$credit_total_applied += $payment->paymentables()
->where('paymentable_type', App\Models\Credit::class)
->selectRaw('sum(paymentables.amount - paymentables.refunded) as p')
->pluck('p')
->first();
} }
if ($credit_total_applied < 0) { if ($credit_total_applied < 0) {
$total_invoice_payments += $credit_total_applied; $total_invoice_payments += $credit_total_applied;
} }
if (round($total_invoice_payments, 2) != round($client->paid_to_date, 2)) { if (round($total_invoice_payments, 2) != round($client->paid_to_date, 2)) {
$wrong_paid_to_dates++; $this->wrong_paid_to_dates++;
$this->logMessage($client->present()->name.' id = # '.$client->id." - Paid to date does not match Client Paid To Date = {$client->paid_to_date} - Invoice Payments = {$total_invoice_payments}"); $this->logMessage($client->present()->name.' id = # '.$client->id." - Paid to date does not match Client Paid To Date = {$client->paid_to_date} - Invoice Payments = {$total_invoice_payments}");
$this->isValid = false; $this->isValid = false;
if($this->option('paid_to_date')){
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." Fixing {$client->paid_to_date} to {$total_invoice_payments}");
$client->paid_to_date = $total_invoice_payments;
$client->save();
}
} }
}); });
$this->logMessage("{$wrong_paid_to_dates} clients with incorrect paid to dates"); $this->logMessage("{$this->wrong_paid_to_dates} clients with incorrect paid to dates");
} }
private function checkInvoicePayments() private function checkInvoicePayments()
{ {
$wrong_balances = 0; $this->wrong_balances = 0;
$wrong_paid_to_dates = 0;
Client::cursor()->where('is_deleted', 0)->each(function ($client) use ($wrong_balances) { Client::cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->each(function ($client) {
$client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($wrong_balances, $client) { $client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($client) {
$total_amount = $invoice->payments()->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED])->get()->sum('pivot.amount');
$total_refund = $invoice->payments()->get()->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED])->sum('pivot.refunded'); $total_paid = $invoice->payments()
->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->selectRaw('sum(paymentables.amount - paymentables.refunded) as p')
->pluck('p')
->first();
// $total_paid = $total_amount - $total_refund;
$total_credit = $invoice->credits()->get()->sum('amount'); $total_credit = $invoice->credits()->get()->sum('amount');
$total_paid = $total_amount - $total_refund;
$calculated_paid_amount = $invoice->amount - $invoice->balance - $total_credit; $calculated_paid_amount = $invoice->amount - $invoice->balance - $total_credit;
if ((string)$total_paid != (string)($invoice->amount - $invoice->balance - $total_credit)) { if ((string)$total_paid != (string)($invoice->amount - $invoice->balance - $total_credit)) {
$wrong_balances++; $this->wrong_balances++;
$this->logMessage($client->present()->name.' - '.$client->id." - Total Amount = {$total_amount} != Calculated Total = {$calculated_paid_amount} - Total Refund = {$total_refund} Total credit = {$total_credit}"); $this->logMessage($client->present()->name.' - '.$client->id." - Total Paid = {$total_paid} != Calculated Total = {$calculated_paid_amount}");
$this->isValid = false; $this->isValid = false;
} }
@ -367,15 +450,47 @@ class CheckData extends Command
}); });
$this->logMessage("{$wrong_balances} clients with incorrect invoice balances"); $this->logMessage("{$this->wrong_balances} clients with incorrect invoice balances");
} }
// $clients = DB::table('clients')
// ->leftJoin('invoices', function($join){
// $join->on('invoices.client_id', '=', 'clients.id')
// ->where('invoices.is_deleted',0)
// ->where('invoices.status_id', '>', 1);
// })
// ->leftJoin('credits', function($join){
// $join->on('credits.client_id', '=', 'clients.id')
// ->where('credits.is_deleted',0)
// ->where('credits.status_id', '>', 1);
// })
// ->leftJoin('payments', function($join) {
// $join->on('payments.client_id', '=', 'clients.id')
// ->where('payments.is_deleted', 0)
// ->whereIn('payments.status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]);
// })
// ->where('clients.is_deleted',0)
// //->where('clients.updated_at', '>', now()->subDays(2))
// ->groupBy('clients.id')
// ->havingRaw('sum(coalesce(invoices.amount - invoices.balance - credits.amount)) != sum(coalesce(payments.amount - payments.refunded, 0))')
// ->get(['clients.id', DB::raw('sum(coalesce(invoices.amount - invoices.balance - credits.amount)) as invoice_amount'), DB::raw('sum(coalesce(payments.amount - payments.refunded, 0)) as payment_amount')]);
private function checkClientBalances() private function checkClientBalances()
{ {
$wrong_balances = 0; $this->wrong_balances = 0;
$wrong_paid_to_dates = 0; $this->wrong_paid_to_dates = 0;
foreach (Client::cursor()->where('is_deleted', 0) as $client) { foreach (Client::cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2)) as $client) {
//$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance'); //$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$invoice_balance = Invoice::where('client_id', $client->id)->where('is_deleted', false)->where('status_id', '>', 1)->withTrashed()->sum('balance'); $invoice_balance = Invoice::where('client_id', $client->id)->where('is_deleted', false)->where('status_id', '>', 1)->withTrashed()->sum('balance');
$credit_balance = Credit::where('client_id', $client->id)->where('is_deleted', false)->withTrashed()->sum('balance'); $credit_balance = Credit::where('client_id', $client->id)->where('is_deleted', false)->withTrashed()->sum('balance');
@ -387,14 +502,15 @@ class CheckData extends Command
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
if ($ledger && (string) $invoice_balance != (string) $client->balance) { if ($ledger && (string) $invoice_balance != (string) $client->balance) {
$wrong_paid_to_dates++; $this->wrong_paid_to_dates++;
$this->logMessage($client->present()->name.' - '.$client->id." - calculated client balances do not match Invoice Balances = {$invoice_balance} - Client Balance = ".rtrim($client->balance, '0'). " Ledger balance = {$ledger->balance}"); $this->logMessage($client->present()->name.' - '.$client->id." - calculated client balances do not match Invoice Balances = {$invoice_balance} - Client Balance = ".rtrim($client->balance, '0'). " Ledger balance = {$ledger->balance}");
$this->isValid = false; $this->isValid = false;
} }
} }
$this->logMessage("{$wrong_paid_to_dates} clients with incorrect client balances"); $this->logMessage("{$this->wrong_paid_to_dates} clients with incorrect client balances");
} }
//fix for client balances = //fix for client balances =
@ -406,27 +522,38 @@ class CheckData extends Command
private function checkInvoiceBalances() private function checkInvoiceBalances()
{ {
$wrong_balances = 0; $this->wrong_balances = 0;
$wrong_paid_to_dates = 0; $this->wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->cursor() as $client) { foreach (Client::where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->cursor() as $client) {
$invoice_balance = $client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get()->sum('balance'); $invoice_balance = $client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$credit_balance = $client->credits()->where('is_deleted', false)->get()->sum('balance'); $credit_balance = $client->credits()->where('is_deleted', false)->sum('balance');
// if($client->balance != $invoice_balance)
// $invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
if ($ledger && number_format($invoice_balance, 4) != number_format($client->balance, 4)) { if ($ledger && number_format($invoice_balance, 4) != number_format($client->balance, 4)) {
$wrong_balances++; $this->wrong_balances++;
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}"); $this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}");
$this->isValid = false; $this->isValid = false;
if($this->option('client_balance')){
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}");
$client->balance = $invoice_balance;
$client->save();
$ledger->adjustment = $invoice_balance;
$ledger->balance = $invoice_balance;
$ledger->notes = 'Ledger Adjustment';
$ledger->save();
}
} }
} }
$this->logMessage("{$wrong_balances} clients with incorrect balances"); $this->logMessage("{$this->wrong_balances} clients with incorrect balances");
} }
private function checkLogoFiles() private function checkLogoFiles()
@ -482,6 +609,7 @@ class CheckData extends Command
'client', 'client',
'client_contact', 'client_contact',
'payment', 'payment',
'recurring_invoice',
], ],
'invoices' => [ 'invoices' => [
'client', 'client',

View File

@ -91,6 +91,8 @@ class CreateAccount extends Command
$account = Account::factory()->create(); $account = Account::factory()->create();
$company = Company::factory()->create([ $company = Company::factory()->create([
'account_id' => $account->id, 'account_id' => $account->id,
'portal_domain' => config('ninja.app_url'),
'portal_mode' => 'domain',
]); ]);
$account->default_company_id = $company->id; $account->default_company_id = $company->id;

View File

@ -14,9 +14,11 @@ namespace App\Console\Commands;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits; use App\DataMapper\FeesAndLimits;
use App\Events\Invoice\InvoiceWasCreated; use App\Events\Invoice\InvoiceWasCreated;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
use App\Factory\GroupSettingFactory; use App\Factory\GroupSettingFactory;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory; use App\Factory\InvoiceItemFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Factory\SubscriptionFactory; use App\Factory\SubscriptionFactory;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyTaskStatuses;
@ -48,6 +50,7 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use stdClass;
class CreateSingleAccount extends Command class CreateSingleAccount extends Command
{ {
@ -117,7 +120,7 @@ class CreateSingleAccount extends Command
$company->settings = $settings; $company->settings = $settings;
$company->save(); $company->save();
$account->default_company_id = $company->id; $account->default_company_id = $company->id;
$account->save(); $account->save();
@ -165,7 +168,7 @@ class CreateSingleAccount extends Command
TaxRate::factory()->create([ TaxRate::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'name' => 'VAT', 'name' => 'VAT',
'rate' => 17.5 'rate' => 17.5
]); ]);
@ -176,7 +179,7 @@ class CreateSingleAccount extends Command
'name' => 'CA Sales Tax', 'name' => 'CA Sales Tax',
'rate' => 5 'rate' => 5
]); ]);
$this->info('Creating '.$this->count.' clients'); $this->info('Creating '.$this->count.' clients');
@ -225,16 +228,19 @@ class CreateSingleAccount extends Command
$client = $company->clients->random(); $client = $company->clients->random();
$this->info('creating task for client #'.$client->id); $this->info('creating task for client #' . $client->id);
$this->createTask($client); $this->createTask($client);
$client = $company->clients->random(); $client = $company->clients->random();
$this->info('creating project for client #'.$client->id); $this->info('creating project for client #' . $client->id);
$this->createProject($client); $this->createProject($client);
$this->info('creating credit for client #'.$client->id); $this->info('creating credit for client #' . $client->id);
$this->createCredit($client); $this->createCredit($client);
$this->info('creating recurring invoice for client # ' . $client->id);
$this->createRecurringInvoice($client);
} }
$this->createGateways($company, $user); $this->createGateways($company, $user);
@ -249,34 +255,34 @@ class CreateSingleAccount extends Command
$gs->save(); $gs->save();
$p1 = Product::factory()->create([ $p1 = Product::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'product_key' => 'pro_plan', 'product_key' => 'pro_plan',
'notes' => 'The Pro Plan', 'notes' => 'The Pro Plan',
'cost' => 10, 'cost' => 10,
'price' => 10, 'price' => 10,
'quantity' => 1, 'quantity' => 1,
]); ]);
$p2 = Product::factory()->create([ $p2 = Product::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'product_key' => 'enterprise_plan', 'product_key' => 'enterprise_plan',
'notes' => 'The Enterprise Plan', 'notes' => 'The Enterprise Plan',
'cost' => 14, 'cost' => 14,
'price' => 14, 'price' => 14,
'quantity' => 1, 'quantity' => 1,
]); ]);
$p3 = Product::factory()->create([ $p3 = Product::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'product_key' => 'free_plan', 'product_key' => 'free_plan',
'notes' => 'The Free Plan', 'notes' => 'The Free Plan',
'cost' => 0, 'cost' => 0,
'price' => 0, 'price' => 0,
'quantity' => 1, 'quantity' => 1,
]); ]);
$webhook_config = [ $webhook_config = [
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan', 'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
@ -435,6 +441,10 @@ class CreateSingleAccount extends Command
$invoice = $invoice_calc->getInvoice(); $invoice = $invoice_calc->getInvoice();
if ($this->gateway === 'braintree') {
$invoice->amount = 100; // Braintree sandbox only allows payments under 2,000 to complete successfully.
}
$invoice->save(); $invoice->save();
$invoice->service()->createInvitations()->markSent(); $invoice->service()->createInvitations()->markSent();
@ -619,7 +629,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -642,7 +652,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -663,7 +673,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -684,11 +694,141 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.wepay') && ($this->gateway == 'all' || $this->gateway == 'wepay')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '8fdeed552015b3c7b44ed6c8ebd9e992';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.wepay'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.braintree') && ($this->gateway == 'all' || $this->gateway == 'braintree')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = 'f7ec488676d310683fb51802d076d713';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.braintree'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.paytrace.decrypted') && ($this->gateway == 'all' || $this->gateway == 'paytrace')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = 'bbd736b3254b0aabed6ad7fda1298c88';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.paytrace.decrypted'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.mollie') && ($this->gateway == 'all' || $this->gateway == 'mollie')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '1bd651fb213ca0c9d66ae3c336dc77e8';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.mollie'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
$cg->save(); $cg->save();
} }
} }
private function createRecurringInvoice($client)
{
$faker = Factory::create();
$invoice = RecurringInvoiceFactory::create($client->company->id, $client->user->id); //stub the company and user_id
$invoice->client_id = $client->id;
$dateable = Carbon::now()->subDays(rand(0, 90));
$invoice->date = $dateable;
$invoice->line_items = $this->buildLineItems(rand(1, 10));
$invoice->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$invoice->tax_name1 = 'GST';
$invoice->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$invoice->tax_name2 = 'VAT';
$invoice->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$invoice->tax_name3 = 'CA Sales Tax';
$invoice->tax_rate3 = 5;
}
$invoice->custom_value1 = $faker->date;
$invoice->custom_value2 = rand(0, 1) ? 'yes' : 'no';
$invoice->status_id = RecurringInvoice::STATUS_ACTIVE;
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
$invoice->save();
event(new RecurringInvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}
} }

View File

@ -0,0 +1,95 @@
<?php
namespace App\Console\Commands;
use App\Libraries\MultiDB;
use App\Models\Company;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class SubdomainFill extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:subdomain';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Pad subdomains';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$c1 = Company::on('db-ninja-01')->whereNull('subdomain')->orWhere('subdomain', '')->get();
$c2 = Company::on('db-ninja-02')->whereNull('subdomain')->orWhere('subdomain', '')->get();
$c1->each(function ($company){
$company->subdomain = MultiDB::randomSubdomainGenerator();
$company->save();
});
$c2->each(function ($company){
$company->subdomain = MultiDB::randomSubdomainGenerator();
$company->save();
});
// $db1 = Company::on('db-ninja-01')->get();
// $db1->each(function ($company){
// $db2 = Company::on('db-ninja-02a')->find($company->id);
// if($db2)
// {
// $db2->subdomain = $company->subdomain;
// $db2->save();
// }
// });
// $db1 = null;
// $db2 = null;
// $db2 = Company::on('db-ninja-02')->get();
// $db2->each(function ($company){
// $db1 = Company::on('db-ninja-01a')->find($company->id);
// if($db1)
// {
// $db1->subdomain = $company->subdomain;
// $db1->save();
// }
// });
}
}

View File

@ -64,12 +64,12 @@ class Kernel extends ConsoleKernel
$schedule->job(new AutoBillCron)->dailyAt('00:30')->withoutOverlapping(); $schedule->job(new AutoBillCron)->dailyAt('00:30')->withoutOverlapping();
$schedule->job(new SchedulerCheck)->everyFiveMinutes(); $schedule->job(new SchedulerCheck)->daily()->withoutOverlapping();
/* Run hosted specific jobs */ /* Run hosted specific jobs */
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
$schedule->job(new AdjustEmailQuota)->daily()->withoutOverlapping(); $schedule->job(new AdjustEmailQuota)->dailyAt('23:00')->withoutOverlapping();
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping(); $schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->daily()->withoutOverlapping(); $schedule->command('ninja:check-data --database=db-ninja-02')->daily()->withoutOverlapping();

View File

@ -0,0 +1,51 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericCounter;
class LivePreview extends GenericCounter
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'counter';
/**
* The name of the counter.
* @var string
*/
public $name = 'live_preview.created';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
/**
* The increment amount... should always be
* set to 0.
*
* @var int
*/
public $metric = 0;
}

View File

@ -28,6 +28,7 @@ class CompanySettings extends BaseSettings
public $lock_invoices = 'off'; //off,when_sent,when_paid //@implemented public $lock_invoices = 'off'; //off,when_sent,when_paid //@implemented
public $enable_client_portal_tasks = false; //@ben to implement public $enable_client_portal_tasks = false; //@ben to implement
public $show_all_tasks_client_portal = 'invoiced'; // all, uninvoiced, invoiced
public $enable_client_portal_password = false; //@implemented public $enable_client_portal_password = false; //@implemented
public $enable_client_portal = true; //@implemented public $enable_client_portal = true; //@implemented
public $enable_client_portal_dashboard = false; // @TODO There currently is no dashboard so this is pending public $enable_client_portal_dashboard = false; // @TODO There currently is no dashboard so this is pending
@ -242,7 +243,7 @@ class CompanySettings extends BaseSettings
public $font_size = 7; //@implemented public $font_size = 7; //@implemented
public $primary_font = 'Roboto'; public $primary_font = 'Roboto';
public $secondary_font = 'Roboto'; public $secondary_font = 'Roboto';
public $primary_color = '#142cb5'; public $primary_color = '#298AAB';
public $secondary_color = '#7081e0'; public $secondary_color = '#7081e0';
public $hide_paid_to_date = false; //@TODO where? public $hide_paid_to_date = false; //@TODO where?
@ -268,6 +269,7 @@ class CompanySettings extends BaseSettings
public $hide_empty_columns_on_pdf = false; public $hide_empty_columns_on_pdf = false;
public static $casts = [ public static $casts = [
'show_all_tasks_client_portal' => 'string',
'entity_send_time' => 'int', 'entity_send_time' => 'int',
'shared_invoice_credit_counter' => 'bool', 'shared_invoice_credit_counter' => 'bool',
'reply_to_name' => 'string', 'reply_to_name' => 'string',
@ -396,7 +398,6 @@ class CompanySettings extends BaseSettings
'email_template_reminder2' => 'string', 'email_template_reminder2' => 'string',
'email_template_reminder3' => 'string', 'email_template_reminder3' => 'string',
'email_template_reminder_endless' => 'string', 'email_template_reminder_endless' => 'string',
'enable_client_portal_password' => 'bool',
'inclusive_taxes' => 'bool', 'inclusive_taxes' => 'bool',
'invoice_number_pattern' => 'string', 'invoice_number_pattern' => 'string',
'invoice_number_counter' => 'integer', 'invoice_number_counter' => 'integer',
@ -662,6 +663,7 @@ class CompanySettings extends BaseSettings
'$task.line_total', '$task.line_total',
], ],
'total_columns' => [ 'total_columns' => [
'$net_subtotal',
'$subtotal', '$subtotal',
'$discount', '$discount',
'$custom_surcharge1', '$custom_surcharge1',

View File

@ -19,6 +19,8 @@ class InvoiceItem
public $product_key = ''; public $product_key = '';
public $product_cost = 0;
public $notes = ''; public $notes = '';
public $discount = 0; public $discount = 0;
@ -57,6 +59,7 @@ class InvoiceItem
'type_id' => 'string', 'type_id' => 'string',
'quantity' => 'float', 'quantity' => 'float',
'cost' => 'float', 'cost' => 'float',
'product_cost' => 'float',
'product_key' => 'string', 'product_key' => 'string',
'notes' => 'string', 'notes' => 'string',
'discount' => 'float', 'discount' => 'float',

View File

@ -27,4 +27,7 @@ class PaymentMethodMeta
/** @var int */ /** @var int */
public $type; public $type;
/** @var string */
public $state;
} }

View File

@ -14,6 +14,7 @@ namespace App\Exceptions;
use App\Exceptions\FilePermissionsFailure; use App\Exceptions\FilePermissionsFailure;
use App\Exceptions\InternalPDFFailure; use App\Exceptions\InternalPDFFailure;
use App\Exceptions\PhantomPDFFailure; use App\Exceptions\PhantomPDFFailure;
use App\Exceptions\StripeConnectFailure;
use App\Utils\Ninja; use App\Utils\Ninja;
use Exception; use Exception;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
@ -77,21 +78,27 @@ class Handler extends ExceptionHandler
return; return;
} }
if(Ninja::isHosted()){ if(Ninja::isHosted() && !($exception instanceof ValidationException)){
app('sentry')->configureScope(function (Scope $scope): void { app('sentry')->configureScope(function (Scope $scope): void {
if(auth()->guard('contact') && auth()->guard('contact')->user()) $name = 'hosted@invoiceninja.com';
if(auth()->guard('contact') && auth()->guard('contact')->user()){
$name = "Contact = ".auth()->guard('contact')->user()->email;
$key = auth()->guard('contact')->user()->company->account->key; $key = auth()->guard('contact')->user()->company->account->key;
elseif (auth()->guard('user') && auth()->guard('user')->user()) }
elseif (auth()->guard('user') && auth()->guard('user')->user()){
$name = "Admin = ".auth()->guard('user')->user()->email;
$key = auth()->user()->account->key; $key = auth()->user()->account->key;
}
else else
$key = 'Anonymous'; $key = 'Anonymous';
$scope->setUser([ $scope->setUser([
'id' => 'Hosted_User', 'id' => $key,
'email' => 'hosted@invoiceninja.com', 'email' => 'hosted@invoiceninja.com',
'name' => $key, 'name' => $name,
]); ]);
}); });
@ -120,8 +127,7 @@ class Handler extends ExceptionHandler
} }
} }
// if(config('ninja.expanded_logging')) parent::report($exception);
parent::report($exception);
} }
@ -182,7 +188,7 @@ class Handler extends ExceptionHandler
} elseif ($exception instanceof NotFoundHttpException && $request->expectsJson()) { } elseif ($exception instanceof NotFoundHttpException && $request->expectsJson()) {
return response()->json(['message'=>'Route does not exist'], 404); return response()->json(['message'=>'Route does not exist'], 404);
} elseif ($exception instanceof MethodNotAllowedHttpException && $request->expectsJson()) { } elseif ($exception instanceof MethodNotAllowedHttpException && $request->expectsJson()) {
return response()->json(['message'=>'Method not support for this route'], 404); return response()->json(['message'=>'Method not supported for this route'], 404);
} elseif ($exception instanceof ValidationException && $request->expectsJson()) { } elseif ($exception instanceof ValidationException && $request->expectsJson()) {
nlog($exception->validator->getMessageBag()); nlog($exception->validator->getMessageBag());
return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422); return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422);
@ -191,7 +197,9 @@ class Handler extends ExceptionHandler
} elseif ($exception instanceof GenericPaymentDriverFailure && $request->expectsJson()) { } elseif ($exception instanceof GenericPaymentDriverFailure && $request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 400); return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof GenericPaymentDriverFailure) { } elseif ($exception instanceof GenericPaymentDriverFailure) {
$data['message'] = $exception->getMessage(); return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof StripeConnectFailure) {
return response()->json(['message' => $exception->getMessage()], 400);
} }
return parent::render($request, $exception); return parent::render($request, $exception);

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class StripeConnectFailure extends Exception
{
// ..
}

View File

@ -28,6 +28,10 @@ class CloneQuoteToInvoiceFactory
unset($quote_array['invoice_id']); unset($quote_array['invoice_id']);
unset($quote_array['id']); unset($quote_array['id']);
unset($quote_array['invitations']); unset($quote_array['invitations']);
unset($quote_array['terms']);
unset($quote_array['public_notes']);
unset($quote_array['footer']);
unset($quote_array['design_id']);
foreach ($quote_array as $key => $value) { foreach ($quote_array as $key => $value) {
$invoice->{$key} = $value; $invoice->{$key} = $value;

View File

@ -22,7 +22,7 @@ class ExpenseCategoryFactory
$expense->company_id = $company_id; $expense->company_id = $company_id;
$expense->name = ''; $expense->name = '';
$expense->is_deleted = false; $expense->is_deleted = false;
$expense->color = '#fff'; $expense->color = '';
return $expense; return $expense;
} }

View File

@ -29,7 +29,7 @@ class RecurringInvoiceToInvoiceFactory
$invoice->public_notes = $recurring_invoice->public_notes; $invoice->public_notes = $recurring_invoice->public_notes;
$invoice->private_notes = $recurring_invoice->private_notes; $invoice->private_notes = $recurring_invoice->private_notes;
//$invoice->date = now()->format($client->date_format()); //$invoice->date = now()->format($client->date_format());
$invoice->due_date = $recurring_invoice->calculateDueDate($recurring_invoice->next_send_date); //$invoice->due_date = $recurring_invoice->calculateDueDate(now());
$invoice->is_deleted = $recurring_invoice->is_deleted; $invoice->is_deleted = $recurring_invoice->is_deleted;
$invoice->line_items = $recurring_invoice->line_items; $invoice->line_items = $recurring_invoice->line_items;
$invoice->tax_name1 = $recurring_invoice->tax_name1; $invoice->tax_name1 = $recurring_invoice->tax_name1;
@ -38,6 +38,8 @@ class RecurringInvoiceToInvoiceFactory
$invoice->tax_rate2 = $recurring_invoice->tax_rate2; $invoice->tax_rate2 = $recurring_invoice->tax_rate2;
$invoice->tax_name3 = $recurring_invoice->tax_name3; $invoice->tax_name3 = $recurring_invoice->tax_name3;
$invoice->tax_rate3 = $recurring_invoice->tax_rate3; $invoice->tax_rate3 = $recurring_invoice->tax_rate3;
$invoice->total_taxes = $recurring_invoice->total_taxes;
$invoice->subscription_id = $recurring_invoice->subscription_id;
$invoice->custom_value1 = $recurring_invoice->custom_value1; $invoice->custom_value1 = $recurring_invoice->custom_value1;
$invoice->custom_value2 = $recurring_invoice->custom_value2; $invoice->custom_value2 = $recurring_invoice->custom_value2;
$invoice->custom_value3 = $recurring_invoice->custom_value3; $invoice->custom_value3 = $recurring_invoice->custom_value3;

View File

@ -21,7 +21,7 @@ class TaskStatusFactory
$task_status->user_id = $user_id; $task_status->user_id = $user_id;
$task_status->company_id = $company_id; $task_status->company_id = $company_id;
$task_status->name = ''; $task_status->name = '';
$task_status->color = '#fff'; $task_status->color = '';
$task_status->status_order = 9999; $task_status->status_order = 9999;
return $task_status; return $task_status;

View File

@ -76,6 +76,11 @@ class ClientFilters extends QueryFilters
return $this->builder->where('id_number', $id_number); return $this->builder->where('id_number', $id_number);
} }
public function number(string $number):Builder
{
return $this->builder->where('number', $number);
}
/** /**
* Filter based on search text. * Filter based on search text.
* *
@ -92,9 +97,11 @@ class ClientFilters extends QueryFilters
return $this->builder->where(function ($query) use ($filter) { return $this->builder->where(function ($query) use ($filter) {
$query->where('clients.name', 'like', '%'.$filter.'%') $query->where('clients.name', 'like', '%'.$filter.'%')
->orWhere('clients.id_number', 'like', '%'.$filter.'%') ->orWhere('clients.id_number', 'like', '%'.$filter.'%')
->orWhere('client_contacts.first_name', 'like', '%'.$filter.'%') ->orWhereHas('contacts', function ($query) use($filter){
->orWhere('client_contacts.last_name', 'like', '%'.$filter.'%') $query->where('first_name', 'like', '%'.$filter.'%');
->orWhere('client_contacts.email', 'like', '%'.$filter.'%') $query->orWhere('last_name', 'like', '%'.$filter.'%');
$query->orWhere('email', 'like', '%'.$filter.'%');
})
->orWhere('clients.custom_value1', 'like', '%'.$filter.'%') ->orWhere('clients.custom_value1', 'like', '%'.$filter.'%')
->orWhere('clients.custom_value2', 'like', '%'.$filter.'%') ->orWhere('clients.custom_value2', 'like', '%'.$filter.'%')
->orWhere('clients.custom_value3', 'like', '%'.$filter.'%') ->orWhere('clients.custom_value3', 'like', '%'.$filter.'%')

View File

@ -38,9 +38,11 @@ class ExpenseFilters extends QueryFilters
return $this->builder->where(function ($query) use ($filter) { return $this->builder->where(function ($query) use ($filter) {
$query->where('expenses.name', 'like', '%'.$filter.'%') $query->where('expenses.name', 'like', '%'.$filter.'%')
->orWhere('expenses.id_number', 'like', '%'.$filter.'%') ->orWhere('expenses.id_number', 'like', '%'.$filter.'%')
->orWhere('expense_contacts.first_name', 'like', '%'.$filter.'%') ->orWhereHas('contacts', function ($query) use($filter){
->orWhere('expense_contacts.last_name', 'like', '%'.$filter.'%') $query->where('expense_contacts.first_name', 'like', '%'.$filter.'%');
->orWhere('expense_contacts.email', 'like', '%'.$filter.'%') $query->orWhere('expense_contacts.last_name', 'like', '%'.$filter.'%');
$query->orWhere('expense_contacts.email', 'like', '%'.$filter.'%');
})
->orWhere('expenses.custom_value1', 'like', '%'.$filter.'%') ->orWhere('expenses.custom_value1', 'like', '%'.$filter.'%')
->orWhere('expenses.custom_value2', 'like', '%'.$filter.'%') ->orWhere('expenses.custom_value2', 'like', '%'.$filter.'%')
->orWhere('expenses.custom_value3', 'like', '%'.$filter.'%') ->orWhere('expenses.custom_value3', 'like', '%'.$filter.'%')

View File

@ -13,6 +13,7 @@ namespace App\Filters;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\User; use App\Models\User;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -21,6 +22,7 @@ use Illuminate\Support\Carbon;
*/ */
class InvoiceFilters extends QueryFilters class InvoiceFilters extends QueryFilters
{ {
use MakesHash;
/** /**
* Filter based on client status. * Filter based on client status.
* *
@ -65,6 +67,18 @@ class InvoiceFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
public function client_id(string $client_id = '') :Builder
{
if (strlen($client_id) == 0) {
return $this->builder;
}
$this->builder->where('client_id', $this->decodePrimaryKey($client_id));
return $this->builder;
}
public function number(string $number) :Builder public function number(string $number) :Builder
{ {
return $this->builder->where('number', $number); return $this->builder->where('number', $number);
@ -96,6 +110,7 @@ class InvoiceFilters extends QueryFilters
}); });
} }
/** /**
* Filters the list based on the status * Filters the list based on the status
* archived, active, deleted - legacy from V1. * archived, active, deleted - legacy from V1.

View File

@ -38,9 +38,11 @@ class VendorFilters extends QueryFilters
return $this->builder->where(function ($query) use ($filter) { return $this->builder->where(function ($query) use ($filter) {
$query->where('vendors.name', 'like', '%'.$filter.'%') $query->where('vendors.name', 'like', '%'.$filter.'%')
->orWhere('vendors.id_number', 'like', '%'.$filter.'%') ->orWhere('vendors.id_number', 'like', '%'.$filter.'%')
->orWhere('vendor_contacts.first_name', 'like', '%'.$filter.'%') ->orWhereHas('contacts', function ($query) use($filter){
->orWhere('vendor_contacts.last_name', 'like', '%'.$filter.'%') $query->where('vendor_contacts.first_name', 'like', '%'.$filter.'%');
->orWhere('vendor_contacts.email', 'like', '%'.$filter.'%') $query->orWhere('vendor_contacts.last_name', 'like', '%'.$filter.'%');
$query->orWhere('vendor_contacts.email', 'like', '%'.$filter.'%');
})
->orWhere('vendors.custom_value1', 'like', '%'.$filter.'%') ->orWhere('vendors.custom_value1', 'like', '%'.$filter.'%')
->orWhere('vendors.custom_value2', 'like', '%'.$filter.'%') ->orWhere('vendors.custom_value2', 'like', '%'.$filter.'%')
->orWhere('vendors.custom_value3', 'like', '%'.$filter.'%') ->orWhere('vendors.custom_value3', 'like', '%'.$filter.'%')

View File

@ -1,5 +1,4 @@
<?php <?php
/** /**
* Invoice Ninja (https://invoiceninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
@ -10,6 +9,8 @@
* @license https://www.elastic.co/licensing/elastic-license * @license https://www.elastic.co/licensing/elastic-license
*/ */
use App\Utils\Ninja;
/** /**
* Simple helper function that will log into "invoiceninja.log" file * Simple helper function that will log into "invoiceninja.log" file
* only when extended logging is enabled. * only when extended logging is enabled.
@ -32,7 +33,16 @@ function nlog($output, $context = []): void
$trace = debug_backtrace(); $trace = debug_backtrace();
//nlog( debug_backtrace()[1]['function']); //nlog( debug_backtrace()[1]['function']);
// \Illuminate\Support\Facades\Log::channel('invoiceninja')->info(print_r($trace[1]['class'],1), []); // \Illuminate\Support\Facades\Log::channel('invoiceninja')->info(print_r($trace[1]['class'],1), []);
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context); if(Ninja::isHosted()) {
try{
info($output);
}
catch(\Exception $e){
}
}
else
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context);
} }

View File

@ -92,6 +92,7 @@ class InvoiceItemSum
private function sumLineItem() private function sumLineItem()
{ //todo need to support quantities less than the precision amount { //todo need to support quantities less than the precision amount
// $this->setLineTotal($this->formatValue($this->item->cost, $this->currency->precision) * $this->formatValue($this->item->quantity, $this->currency->precision)); // $this->setLineTotal($this->formatValue($this->item->cost, $this->currency->precision) * $this->formatValue($this->item->quantity, $this->currency->precision));
$this->setLineTotal($this->item->cost * $this->item->quantity); $this->setLineTotal($this->item->cost * $this->item->quantity);
return $this; return $this;
@ -102,7 +103,15 @@ class InvoiceItemSum
if ($this->invoice->is_amount_discount) { if ($this->invoice->is_amount_discount) {
$this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision)); $this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision));
} else { } else {
$this->setLineTotal($this->getLineTotal() - $this->formatValue(round($this->item->line_total * ($this->item->discount / 100), 2), $this->currency->precision));
/*Test 16-08-2021*/
$discount = ($this->item->line_total * ($this->item->discount / 100));
$this->setLineTotal($this->formatValue(($this->getLineTotal() - $discount), $this->currency->precision));
/*Test 16-08-2021*/
//replaces the following
// $this->setLineTotal($this->getLineTotal() - $this->formatValue(round($this->item->line_total * ($this->item->discount / 100), 2), $this->currency->precision));
} }
$this->item->is_amount_discount = $this->invoice->is_amount_discount; $this->item->is_amount_discount = $this->invoice->is_amount_discount;
@ -114,9 +123,6 @@ class InvoiceItemSum
{ {
$item_tax = 0; $item_tax = 0;
// nlog(print_r($this->item,1));
// nlog(print_r($this->invoice,1));
$amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / 100)); $amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / 100));
$item_tax_rate1_total = $this->calcAmountLineTax($this->item->tax_rate1, $amount); $item_tax_rate1_total = $this->calcAmountLineTax($this->item->tax_rate1, $amount);

View File

@ -46,8 +46,6 @@ class GmailTransport extends Transport
$this->gmail = null; $this->gmail = null;
$this->gmail = new Mail; $this->gmail = new Mail;
nlog($message->getBcc());
/*We should nest the token in the message and then discard it as needed*/ /*We should nest the token in the message and then discard it as needed*/
$token = $message->getHeaders()->get('GmailToken')->getValue(); $token = $message->getHeaders()->get('GmailToken')->getValue();
@ -62,13 +60,13 @@ class GmailTransport extends Transport
$this->gmail->message($message->getBody()); $this->gmail->message($message->getBody());
$this->gmail->cc($message->getCc()); $this->gmail->cc($message->getCc());
$this->gmail->bcc($message->getBcc());
if(is_array($message->getBcc()))
$this->gmail->bcc(array_keys($message->getBcc()));
foreach ($message->getChildren() as $child) foreach ($message->getChildren() as $child)
{ {
nlog("trying to attach");
if($child->getContentType() != 'text/plain') if($child->getContentType() != 'text/plain')
{ {

View File

@ -163,6 +163,6 @@ class ActivityController extends BaseController
return response()->streamDownload(function () use ($pdf) { return response()->streamDownload(function () use ($pdf) {
echo $pdf; echo $pdf;
}, $filename); }, $filename, ['Content-Type' => 'application/pdf']);
} }
} }

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Contact\ContactPasswordResetRequest;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\Account; use App\Models\Account;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
@ -73,10 +74,8 @@ class ContactForgotPasswordController extends Controller
return Password::broker('contacts'); return Password::broker('contacts');
} }
public function sendResetLinkEmail(Request $request) public function sendResetLinkEmail(ContactPasswordResetRequest $request)
{ {
//MultiDB::userFindAndSetDb($request->input('email'));
$user = MultiDB::hasContact($request->input('email')); $user = MultiDB::hasContact($request->input('email'));
$this->validateEmail($request); $this->validateEmail($request);

View File

@ -35,9 +35,16 @@ class ContactLoginController extends Controller
public function showLoginForm(Request $request) public function showLoginForm(Request $request)
{ {
if ($request->subdomain) { //if we are on the root domain invoicing.co do not show any company logos
$company = Company::where('subdomain', $request->subdomain)->first(); if(Ninja::isHosted() && count(explode('.', request()->getHost())) == 2){
} elseif (Ninja::isSelfHost()) { $company = null;
}elseif (strpos($request->getHost(), 'invoicing.co') !== false) {
$subdomain = explode('.', $request->getHost())[0];
$company = Company::where('subdomain', $subdomain)->first();
} elseif(Ninja::isHosted() && $company = Company::where('portal_domain', $request->getSchemeAndHttpHost())->first()){
}
elseif (Ninja::isSelfHost()) {
$company = Account::first()->default_company; $company = Account::first()->default_company;
} else { } else {
$company = null; $company = null;

View File

@ -1,4 +1,13 @@
<?php <?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;

View File

@ -71,6 +71,31 @@ class ContactResetPasswordController extends Controller
); );
} }
public function reset(Request $request)
{
$request->validate($this->rules(), $this->validationErrorMessages());
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
// Added this because it collides the session between
// client & main portal giving unlimited redirects.
auth()->logout();
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $response == Password::PASSWORD_RESET
? $this->sendResetResponse($request, $response)
: $this->sendResetFailedResponse($request, $response);
}
protected function guard() protected function guard()
{ {
return Auth::guard('contact'); return Auth::guard('contact');

View File

@ -16,6 +16,7 @@ use App\DataMapper\Analytics\LoginSuccess;
use App\Events\User\UserLoggedIn; use App\Events\User\UserLoggedIn;
use App\Http\Controllers\BaseController; use App\Http\Controllers\BaseController;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Login\LoginRequest;
use App\Jobs\Account\CreateAccount; use App\Jobs\Account\CreateAccount;
use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Company\CreateCompanyToken;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
@ -156,7 +157,7 @@ class LoginController extends BaseController
* ), * ),
* ) * )
*/ */
public function apiLogin(Request $request) public function apiLogin(LoginRequest $request)
{ {
$this->forced_includes = ['company_users']; $this->forced_includes = ['company_users'];
@ -179,8 +180,6 @@ class LoginController extends BaseController
$user = $this->guard()->user(); $user = $this->guard()->user();
event(new UserLoggedIn($user, $user->account->default_company, Ninja::eventVars($user->id)));
//2FA //2FA
if($user->google_2fa_secret && $request->has('one_time_password')) if($user->google_2fa_secret && $request->has('one_time_password'))
{ {
@ -203,6 +202,14 @@ class LoginController extends BaseController
->header('X-Api-Version', config('ninja.minimum_client_version')); ->header('X-Api-Version', config('ninja.minimum_client_version'));
} }
/* If for some reason we lose state on the default company ie. a company is deleted - always make sure we can default to a company*/
if(!$user->account->default_company){
$account = $user->account;
$account->default_company_id = $user->companies->first()->id;
$account->save();
$user = $user->fresh();
}
$user->setCompany($user->account->default_company); $user->setCompany($user->account->default_company);
$this->setLoginCache($user); $this->setLoginCache($user);
@ -226,6 +233,8 @@ class LoginController extends BaseController
if(Ninja::isHosted() && !$cu->first()->is_owner && !$user->account->isEnterpriseClient()) if(Ninja::isHosted() && !$cu->first()->is_owner && !$user->account->isEnterpriseClient())
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
event(new UserLoggedIn($user, $user->account->default_company, Ninja::eventVars($user->id)));
return $this->timeConstrainedResponse($cu); return $this->timeConstrainedResponse($cu);
@ -300,7 +309,6 @@ class LoginController extends BaseController
$cu = CompanyUser::query() $cu = CompanyUser::query()
->where('user_id', $company_token->user_id); ->where('user_id', $company_token->user_id);
$cu->first()->account->companies->each(function ($company) use($cu, $request){ $cu->first()->account->companies->each(function ($company) use($cu, $request){
if($company->tokens()->where('is_system', true)->count() == 0) if($company->tokens()->where('is_system', true)->count() == 0)
@ -309,7 +317,6 @@ class LoginController extends BaseController
} }
}); });
if($request->has('current_company') && $request->input('current_company') == 'true') if($request->has('current_company') && $request->input('current_company') == 'true')
$cu->where("company_id", $company_token->company_id); $cu->where("company_id", $company_token->company_id);
@ -361,6 +368,9 @@ class LoginController extends BaseController
if ($existing_user = MultiDB::hasUser($query)) { if ($existing_user = MultiDB::hasUser($query)) {
if(!$existing_user->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_user, true); Auth::login($existing_user, true);
$existing_user->setCompany($existing_user->account->default_company); $existing_user->setCompany($existing_user->account->default_company);
@ -387,6 +397,9 @@ class LoginController extends BaseController
//If this is a result user/email combo - lets add their OAuth details details //If this is a result user/email combo - lets add their OAuth details details
if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)])) if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)]))
{ {
if(!$existing_login_user->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_login_user, true); Auth::login($existing_login_user, true);
$existing_login_user->setCompany($existing_login_user->account->default_company); $existing_login_user->setCompany($existing_login_user->account->default_company);
@ -422,6 +435,9 @@ class LoginController extends BaseController
if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)])) if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)]))
{ {
if(!$existing_login_user->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_login_user, true); Auth::login($existing_login_user, true);
$existing_login_user->setCompany($existing_login_user->account->default_company); $existing_login_user->setCompany($existing_login_user->account->default_company);
@ -473,6 +489,8 @@ class LoginController extends BaseController
auth()->user()->email_verified_at = now(); auth()->user()->email_verified_at = now();
auth()->user()->save(); auth()->user()->save();
auth()->user()->setCompany(auth()->user()->account->default_company);
$this->setLoginCache(auth()->user()); $this->setLoginCache(auth()->user());
$cu = CompanyUser::whereUserId(auth()->user()->id); $cu = CompanyUser::whereUserId(auth()->user()->id);

View File

@ -112,6 +112,7 @@ class BaseController extends Controller
'company.groups', 'company.groups',
'company.payment_terms', 'company.payment_terms',
'company.designs.company', 'company.designs.company',
'company.expense_categories',
]; ];
public function __construct() public function __construct()
@ -202,6 +203,10 @@ class BaseController extends Controller
$transformer = new $this->entity_transformer($this->serializer); $transformer = new $this->entity_transformer($this->serializer);
$updated_at = request()->has('updated_at') ? request()->input('updated_at') : 0; $updated_at = request()->has('updated_at') ? request()->input('updated_at') : 0;
if ($user->getCompany()->is_large && $updated_at == 0){
$updated_at = time();
}
$updated_at = date('Y-m-d H:i:s', $updated_at); $updated_at = date('Y-m-d H:i:s', $updated_at);
$query->with( $query->with(
@ -379,8 +384,6 @@ class BaseController extends Controller
'company.designs'=> function ($query) use ($created_at, $user) { 'company.designs'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('company'); $query->where('created_at', '>=', $created_at)->with('company');
if(!$user->isAdmin())
$query->where('designs.user_id', $user->id);
}, },
'company.documents'=> function ($query) use ($created_at, $user) { 'company.documents'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
@ -388,22 +391,14 @@ class BaseController extends Controller
'company.groups' => function ($query) use ($created_at, $user) { 'company.groups' => function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
if(!$user->isAdmin())
$query->where('group_settings.user_id', $user->id);
}, },
'company.payment_terms'=> function ($query) use ($created_at, $user) { 'company.payment_terms'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
if(!$user->isAdmin())
$query->where('payment_terms.user_id', $user->id);
}, },
'company.tax_rates' => function ($query) use ($created_at, $user) { 'company.tax_rates' => function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
if(!$user->isAdmin())
$query->where('tax_rates.user_id', $user->id);
}, },
'company.activities'=> function ($query) use($user) { 'company.activities'=> function ($query) use($user) {
@ -519,8 +514,6 @@ class BaseController extends Controller
'company.payment_terms'=> function ($query) use ($created_at, $user) { 'company.payment_terms'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
if(!$user->isAdmin())
$query->where('payment_terms.user_id', $user->id);
}, },
'company.products' => function ($query) use ($created_at, $user) { 'company.products' => function ($query) use ($created_at, $user) {
@ -561,9 +554,6 @@ class BaseController extends Controller
'company.tax_rates' => function ($query) use ($created_at, $user) { 'company.tax_rates' => function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
if(!$user->isAdmin())
$query->where('tax_rates.user_id', $user->id);
}, },
'company.vendors'=> function ($query) use ($created_at, $user) { 'company.vendors'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('contacts', 'documents'); $query->where('created_at', '>=', $created_at)->with('contacts', 'documents');
@ -575,9 +565,6 @@ class BaseController extends Controller
'company.expense_categories'=> function ($query) use ($created_at, $user) { 'company.expense_categories'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
if(!$user->isAdmin())
$query->where('expense_categories.user_id', $user->id);
}, },
'company.task_statuses'=> function ($query) use ($created_at, $user) { 'company.task_statuses'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
@ -748,11 +735,14 @@ class BaseController extends Controller
$data = []; $data = [];
if (Ninja::isSelfHost()) { //pass report errors bool to front end
$data['report_errors'] = $account->report_errors; $data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true;
} else {
$data['report_errors'] = true; //pass referral code to front end
} $data['rc'] = request()->has('rc') ? request()->input('rc') : '';
$data['build'] = request()->has('build') ? request()->input('build') : '';
$data['path'] = $this->setBuild();
$this->buildCache(); $this->buildCache();
@ -762,6 +752,29 @@ class BaseController extends Controller
return redirect('/setup'); return redirect('/setup');
} }
private function setBuild()
{
$build = '';
if(request()->has('build')) {
$build = request()->input('build');
}
switch ($build) {
case 'wasm':
return 'main.wasm.dart.js';
case 'foss':
return 'main.foss.dart.js';
case 'last':
return 'main.last.dart.js';
case 'next':
return 'main.next.dart.js';
default:
return 'main.dart.js';
}
}
public function checkFeature($feature) public function checkFeature($feature)
{ {

View File

@ -510,7 +510,7 @@ class ClientController extends BaseController
$action = request()->input('action'); $action = request()->input('action');
$ids = request()->input('ids'); $ids = request()->input('ids');
$clients = Client::withTrashed()->find($this->transformKeys($ids)); $clients = Client::withTrashed()->whereIn('id', $this->transformKeys($ids))->cursor();
$clients->each(function ($client, $key) use ($action) { $clients->each(function ($client, $key) use ($action) {
if (auth()->user()->can('edit', $client)) { if (auth()->user()->can('edit', $client)) {

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers\ClientPortal; namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\RecurringInvoice;
use Auth; use Auth;
class ContactHashLoginController extends Controller class ContactHashLoginController extends Controller
@ -24,6 +25,17 @@ class ContactHashLoginController extends Controller
*/ */
public function login(string $contact_key) public function login(string $contact_key)
{ {
if(request()->has('subscription') && $request->subscription == 'true') {
$recurring_invoice = RecurringInvoice::where('client_id', auth()->guard('contact')->client->id)
->whereNotNull('subscription_id')
->whereNull('deleted_at')
->first();
return redirect()->route('client.recurring_invoice.show', $recurring_invoice->hashed_id);
}
return redirect('/client/invoices'); return redirect('/client/invoices');
} }

View File

@ -24,6 +24,7 @@ use Illuminate\Contracts\View\Factory;
use Illuminate\View\View; use Illuminate\View\View;
use ZipStream\Option\Archive; use ZipStream\Option\Archive;
use ZipStream\ZipStream; use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
class InvoiceController extends Controller class InvoiceController extends Controller
{ {
@ -89,6 +90,7 @@ class InvoiceController extends Controller
{ {
$invoices = Invoice::whereIn('id', $ids) $invoices = Invoice::whereIn('id', $ids)
->whereClientId(auth()->user()->client->id) ->whereClientId(auth()->user()->client->id)
->withTrashed()
->get(); ->get();
//filter invoices which are payable //filter invoices which are payable
@ -167,9 +169,12 @@ class InvoiceController extends Controller
if ($invoices->count() == 1) { if ($invoices->count() == 1) {
$invoice = $invoices->first(); $invoice = $invoices->first();
$invitation = $invoice->invitations->first(); $invitation = $invoice->invitations->first();
$file = $invoice->pdf_file_path($invitation); //$file = $invoice->pdf_file_path($invitation);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);; $file = $invoice->service()->getInvoicePdf(auth()->user());
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);;
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
} }
// enable output of HTTP headers // enable output of HTTP headers
@ -180,7 +185,10 @@ class InvoiceController extends Controller
$zip = new ZipStream(date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip', $options); $zip = new ZipStream(date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip', $options);
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
$zip->addFileFromPath(basename($invoice->pdf_file_path()), TempFile::path($invoice->pdf_file_path()));
#add it to the zip
$zip->addFile(basename($invoice->pdf_file_path()), file_get_contents($invoice->pdf_file_path(null, 'url', true)));
} }
// finish the zip stream // finish the zip stream

View File

@ -95,7 +95,7 @@ class PaymentController extends Controller
*/ */
$payable_invoices = collect($request->payable_invoices); $payable_invoices = collect($request->payable_invoices);
$invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->get(); $invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
$invoices->each(function($invoice){ $invoices->each(function($invoice){
$invoice->service()->removeUnpaidGatewayFees()->save(); $invoice->service()->removeUnpaidGatewayFees()->save();
@ -243,6 +243,10 @@ class PaymentController extends Controller
->get(); ->get();
} }
if(!$is_credit_payment){
$credit_totals = 0;
}
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))]; $hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))];
if ($request->query('hash')) { if ($request->query('hash')) {
@ -257,11 +261,19 @@ class PaymentController extends Controller
$payment_hash->save(); $payment_hash->save();
if($is_credit_payment){
$amount_with_fee = max(0, (($invoice_totals + $fee_totals) - $credit_totals));
}
else{
$credit_totals = 0;
$amount_with_fee = max(0, $invoice_totals + $fee_totals);
}
$totals = [ $totals = [
'credit_totals' => $credit_totals, 'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals, 'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals, 'fee_total' => $fee_totals,
'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals)), 'amount_with_fee' => $amount_with_fee,
]; ];
$data = [ $data = [
@ -273,7 +285,7 @@ class PaymentController extends Controller
'amount_with_fee' => $invoice_totals + $fee_totals, 'amount_with_fee' => $invoice_totals + $fee_totals,
]; ];
if ($is_credit_payment) { if ($is_credit_payment || $totals <= 0) {
return $this->processCreditPayment($request, $data); return $this->processCreditPayment($request, $data);
} }
@ -319,7 +331,8 @@ class PaymentController extends Controller
* @return Response The response view * @return Response The response view
*/ */
public function credit_response(Request $request) public function credit_response(Request $request)
{ {
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->first(); $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->first();
/* Hydrate the $payment */ /* Hydrate the $payment */

View File

@ -14,7 +14,7 @@ namespace App\Http\Controllers\ClientPortal;
use App\Events\Payment\Methods\MethodDeleted; use App\Events\Payment\Methods\MethodDeleted;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\CreatePaymentMethodRequest; use App\Http\Requests\ClientPortal\PaymentMethod\CreatePaymentMethodRequest;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
@ -51,6 +51,7 @@ class PaymentMethodController extends Controller
$gateway = $this->getClientGateway(); $gateway = $this->getClientGateway();
$data['gateway'] = $gateway; $data['gateway'] = $gateway;
$data['client'] = auth()->user()->client;
return $gateway return $gateway
->driver(auth()->user()->client) ->driver(auth()->user()->client)
@ -91,9 +92,9 @@ class PaymentMethodController extends Controller
public function verify(ClientGatewayToken $payment_method) public function verify(ClientGatewayToken $payment_method)
{ {
$gateway = $this->getClientGateway(); // $gateway = $this->getClientGateway();
return $gateway return $payment_method->gateway
->driver(auth()->user()->client) ->driver(auth()->user()->client)
->setPaymentMethod(request()->query('method')) ->setPaymentMethod(request()->query('method'))
->verificationView($payment_method); ->verificationView($payment_method);
@ -101,9 +102,9 @@ class PaymentMethodController extends Controller
public function processVerification(Request $request, ClientGatewaytoken $payment_method) public function processVerification(Request $request, ClientGatewaytoken $payment_method)
{ {
$gateway = $this->getClientGateway(); // $gateway = $this->getClientGateway();
return $gateway return $payment_method->gateway
->driver(auth()->user()->client) ->driver(auth()->user()->client)
->setPaymentMethod(request()->query('method')) ->setPaymentMethod(request()->query('method'))
->processVerification($request, $payment_method); ->processVerification($request, $payment_method);
@ -117,16 +118,22 @@ class PaymentMethodController extends Controller
*/ */
public function destroy(ClientGatewayToken $payment_method) public function destroy(ClientGatewayToken $payment_method)
{ {
$gateway = $this->getClientGateway();
$gateway if($payment_method->gateway()->exists()){
->driver(auth()->user()->client)
->setPaymentMethod(request()->query('method')) $payment_method->gateway
->detach($payment_method); ->driver(auth()->user()->client)
->setPaymentMethod(request()->query('method'))
->detach($payment_method);
}
try { try {
event(new MethodDeleted($payment_method, auth('contact')->user()->company, Ninja::eventVars(auth('contact')->user()->id))); event(new MethodDeleted($payment_method, auth('contact')->user()->company, Ninja::eventVars(auth('contact')->user()->id)));
$payment_method->delete(); $payment_method->delete();
} catch (Exception $e) { } catch (Exception $e) {
nlog($e->getMessage()); nlog($e->getMessage());

View File

@ -7,7 +7,7 @@
* *
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://www.elastic.co/licensing/elastic-license
*/ */
namespace App\Http\Controllers\ClientPortal; namespace App\Http\Controllers\ClientPortal;
@ -24,8 +24,10 @@ use App\Utils\TempFile;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\View\View; use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use ZipStream\Option\Archive; use ZipStream\Option\Archive;
use ZipStream\ZipStream; use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
class QuoteController extends Controller class QuoteController extends Controller
{ {
@ -46,7 +48,7 @@ class QuoteController extends Controller
* *
* @param ShowQuoteRequest $request * @param ShowQuoteRequest $request
* @param Quote $quote * @param Quote $quote
* @return Factory|View|\Symfony\Component\HttpFoundation\BinaryFileResponse * @return Factory|View|BinaryFileResponse
*/ */
public function show(ShowQuoteRequest $request, Quote $quote) public function show(ShowQuoteRequest $request, Quote $quote)
{ {
@ -83,13 +85,18 @@ class QuoteController extends Controller
->get(); ->get();
if (! $quotes || $quotes->count() == 0) { if (! $quotes || $quotes->count() == 0) {
return; return redirect()
->route('client.quotes.index')
->with('message', ctrans('texts.no_quotes_available_for_download'));
} }
if ($quotes->count() == 1) { if ($quotes->count() == 1) {
$file = $quotes->first()->pdf_file_path(); $file = $quotes->first()->service()->getQuotePdf();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true); // return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
} }
// enable output of HTTP headers // enable output of HTTP headers
@ -100,7 +107,9 @@ class QuoteController extends Controller
$zip = new ZipStream(date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip', $options); $zip = new ZipStream(date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip', $options);
foreach ($quotes as $quote) { foreach ($quotes as $quote) {
$zip->addFileFromPath(basename($quote->pdf_file_path()), TempFile::path($quote->pdf_file_path())); $zip->addFile(basename($quote->pdf_file_path()), file_get_contents($quote->pdf_file_path(null, 'url', true)));
// $zip->addFileFromPath(basename($quote->pdf_file_path()), TempFile::path($quote->pdf_file_path()));
} }
// finish the zip stream // finish the zip stream
@ -110,11 +119,15 @@ class QuoteController extends Controller
protected function approve(array $ids, $process = false) protected function approve(array $ids, $process = false)
{ {
$quotes = Quote::whereIn('id', $ids) $quotes = Quote::whereIn('id', $ids)
->whereClientId(auth()->user()->client->id) ->where('client_id', auth('contact')->user()->client->id)
->where('company_id', auth('contact')->user()->client->company_id)
->where('status_id', Quote::STATUS_SENT)
->get(); ->get();
if (! $quotes || $quotes->count() == 0) { if (!$quotes || $quotes->count() == 0) {
return redirect()->route('client.quotes.index'); return redirect()
->route('client.quotes.index')
->with('message', ctrans('texts.quotes_with_status_sent_can_be_approved'));
} }
if ($process) { if ($process) {

View File

@ -43,8 +43,7 @@ class SubscriptionPlanSwitchController extends Controller
*/ */
if(is_null($amount)) if(is_null($amount))
render('subscriptions.denied'); render('subscriptions.denied');
return render('subscriptions.switch', [ return render('subscriptions.switch', [
'subscription' => $recurring_invoice->subscription, 'subscription' => $recurring_invoice->subscription,
'recurring_invoice' => $recurring_invoice, 'recurring_invoice' => $recurring_invoice,

View File

@ -25,7 +25,10 @@ use App\Jobs\Company\CreateCompany;
use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyPaymentTerms;
use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Company\CreateCompanyToken;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Ninja\RefundCancelledAccount; use App\Jobs\Ninja\RefundCancelledAccount;
use App\Mail\Company\CompanyDeleted;
use App\Models\Account; use App\Models\Account;
use App\Models\Company; use App\Models\Company;
use App\Models\CompanyUser; use App\Models\CompanyUser;
@ -474,10 +477,16 @@ class CompanyController extends BaseController
*/ */
public function destroy(DestroyCompanyRequest $request, Company $company) public function destroy(DestroyCompanyRequest $request, Company $company)
{ {
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$company_count = $company->account->companies->count(); $company_count = $company->account->companies->count();
$account = $company->account; $account = $company->account;
$account_key = $account->key;
if ($company_count == 1) { if ($company_count == 1) {
$company->company_users->each(function ($company_user) { $company->company_users->each(function ($company_user) {
$company_user->user->forceDelete(); $company_user->user->forceDelete();
$company_user->forceDelete(); $company_user->forceDelete();
@ -485,15 +494,28 @@ class CompanyController extends BaseController
$account->delete(); $account->delete();
if(Ninja::isHosted())
\Modules\Admin\Jobs\Account\NinjaDeletedAccount::dispatch($account_key);
LightLogs::create(new AccountDeleted()) LightLogs::create(new AccountDeleted())
->increment() ->increment()
->batch(); ->batch();
} else { } else {
$company_id = $company->id; $company_id = $company->id;
$company->company_users->each(function ($company_user){ $company->company_users->each(function ($company_user){
$company_user->forceDelete(); $company_user->forceDelete();
}); });
$other_company = $company->account->companies->where('id', '!=', $company->id)->first();
$nmo = new NinjaMailerObject;
$nmo->mailable = new CompanyDeleted($company->present()->name, auth()->user(), $company->account, $company->settings);
$nmo->company = $other_company;
$nmo->settings = $other_company->settings;
$nmo->to_user = auth()->user();
NinjaMailerJob::dispatch($nmo);
$company->delete(); $company->delete();
@ -571,4 +593,9 @@ class CompanyController extends BaseController
return $this->itemResponse($company->fresh()); return $this->itemResponse($company->fresh());
} }
// public function default(DefaultCompanyRequest $request, Company $company)
// {
// }
} }

View File

@ -433,9 +433,14 @@ class CompanyGatewayController extends BaseController
*/ */
public function destroy(DestroyCompanyGatewayRequest $request, CompanyGateway $company_gateway) public function destroy(DestroyCompanyGatewayRequest $request, CompanyGateway $company_gateway)
{ {
$company_gateway->driver(new Client)
->disconnect();
$company_gateway->delete(); $company_gateway->delete();
return $this->itemResponse($company_gateway->fresh()); return $this->itemResponse($company_gateway->fresh());
} }
/** /**

View File

@ -123,8 +123,8 @@ class CompanyUserController extends BaseController
if (auth()->user()->isAdmin()) { if (auth()->user()->isAdmin()) {
$company_user->fill($request->input('company_user')); $company_user->fill($request->input('company_user'));
} else { } else {
$company_user->fill($request->input('company_user')['settings']); $company_user->settings = $request->input('company_user')['settings'];
$company_user->fill($request->input('company_user')['notifications']); $company_user->notifications = $request->input('company_user')['notifications'];
} }
$company_user->save(); $company_user->save();

View File

@ -104,8 +104,13 @@ class ConnectedAccountController extends BaseController
$refresh_token = ''; $refresh_token = '';
$token = ''; $token = '';
$email = $google->harvestEmail($user);
if(auth()->user()->email != $email && MultiDB::checkUserEmailExists($email))
return response()->json(['message' => ctrans('texts.email_already_register')], 400);
$connected_account = [ $connected_account = [
'email' => $google->harvestEmail($user), 'email' => $email,
'oauth_user_id' => $google->harvestSubField($user), 'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id' => 'google', 'oauth_provider_id' => 'google',
'email_verified_at' =>now() 'email_verified_at' =>now()

View File

@ -37,6 +37,7 @@ use App\Utils\TempFile;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments; use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/** /**
* Class CreditController. * Class CreditController.
@ -536,8 +537,14 @@ class CreditController extends BaseController
} }
break; break;
case 'download': case 'download':
$file = $credit->pdf_file_path(); // $file = $credit->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true); $file = $credit->service()->getCreditPdf($credit->invitations->first());
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
break; break;
case 'archive': case 'archive':
$this->credit_repository->archive($credit); $this->credit_repository->archive($credit);
@ -585,9 +592,12 @@ class CreditController extends BaseController
// $contact = $invitation->contact; // $contact = $invitation->contact;
$credit = $invitation->credit; $credit = $invitation->credit;
$file_path = $credit->service()->getCreditPdf($invitation); $file = $credit->service()->getCreditPdf($invitation);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
} }
/** /**

View File

@ -131,8 +131,8 @@ class EmailController extends BaseController
$entity_obj->service()->markSent()->save(); $entity_obj->service()->markSent()->save();
EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data) EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data);
->delay(now()->addSeconds(30)); // ->delay(now()->addSeconds(45));
} }

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Gateways;
use App\Http\Controllers\Controller;
use App\Http\Requests\Gateways\Mollie\Mollie3dsRequest;
use App\Models\PaymentHash;
class Mollie3dsController extends Controller
{
public function index(Mollie3dsRequest $request)
{
return $request->getCompanyGateway()
->driver($request->getClient())
->process3dsConfirmation($request);
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use App\Jobs\Account\CreateAccount;
use App\Libraries\MultiDB;
use App\Models\CompanyToken;
use Illuminate\Http\Request;
class HostedMigrationController extends Controller
{
public function getAccount(Request $request)
{
if($request->header('X-API-HOSTED-SECRET') != config('ninja.ninja_hosted_secret'))
return;
if($user = MultiDB::hasUser(['email' => $request->input('email')]))
{
if($user->account->owner() && $user->account->companies()->count() >= 1)
{
return response()->json(['token' => $user->account->companies->first()->tokens->first()->token] ,200);
}
return response()->json(['error' => 'This user is not able to perform a migration. Please contact us at contact@invoiceninja.com to discuss.'], 401);
}
$account = CreateAccount::dispatchNow($request->all(), $request->getClientIp());
$company = $account->companies->first();
$company_token = CompanyToken::where('user_id', auth()->user()->id)
->where('company_id', $company->id)
->first();
return response()->json(['token' => $company_token->token], 200);
}
}

View File

@ -11,14 +11,17 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Exceptions\NonExistingMigrationFile;
use App\Http\Requests\Import\ImportJsonRequest; use App\Http\Requests\Import\ImportJsonRequest;
use App\Jobs\Company\CompanyExport; use App\Jobs\Company\CompanyExport;
use App\Jobs\Company\CompanyImport; use App\Jobs\Company\CompanyImport;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use ZipArchive; use ZipArchive;
use Illuminate\Support\Facades\Storage;
class ImportJsonController extends BaseController class ImportJsonController extends BaseController
{ {
@ -60,40 +63,19 @@ class ImportJsonController extends BaseController
public function import(ImportJsonRequest $request) public function import(ImportJsonRequest $request)
{ {
$import_file = $request->file('files'); $file_location = $request->file('files')
->storeAs(
'migrations',
$request->file('files')->getClientOriginalName()
);
$contents = $this->unzipFile($import_file->getPathname()); if(Ninja::isHosted())
CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $file_location, $request->except('files'))->onQueue('migration');
$hash = Str::random(32); else
CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $file_location, $request->except('files'));
nlog($hash);
Cache::put( $hash, base64_encode( $contents ), 3600 );
CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $hash, $request->except('files'))->delay(now()->addMinutes(1));
return response()->json(['message' => 'Processing'], 200); return response()->json(['message' => 'Processing'], 200);
} }
private function unzipFile($file_contents)
{
$zip = new ZipArchive();
$archive = $zip->open($file_contents);
$filename = pathinfo($file_contents, PATHINFO_FILENAME);
$zip->extractTo(public_path("storage/backups/{$filename}"));
$zip->close();
$file_location = public_path("storage/backups/$filename/backup.json");
if (! file_exists($file_location))
throw new NonExistingMigrationFile('Backup file does not exist, or is corrupted.');
$data = file_get_contents($file_location);
unlink($file_contents);
unlink($file_location);
return $data;
}
} }

View File

@ -672,8 +672,12 @@ class InvoiceController extends BaseController
break; break;
case 'download': case 'download':
$file = $invoice->pdf_file_path(); $file = $invoice->service()->getInvoicePdf();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
break; break;
case 'restore': case 'restore':
@ -722,10 +726,11 @@ class InvoiceController extends BaseController
} }
//touch reminder1,2,3_sent + last_sent here if the email is a reminder. //touch reminder1,2,3_sent + last_sent here if the email is a reminder.
$invoice->service()->touchReminder($this->reminder_template)->deletePdf()->save(); //$invoice->service()->touchReminder($this->reminder_template)->deletePdf()->save();
$invoice->service()->touchReminder($this->reminder_template)->markSent()->save();
$invoice->invitations->load('contact.client.country', 'invoice.client.country', 'invoice.company')->each(function ($invitation) use ($invoice) { $invoice->invitations->load('contact.client.country', 'invoice.client.country', 'invoice.company')->each(function ($invitation) use ($invoice) {
EmailEntity::dispatch($invitation, $invoice->company, $this->reminder_template); EmailEntity::dispatch($invitation, $invoice->company, $this->reminder_template)->delay(now()->addSeconds(30));
}); });
if ($invoice->invitations->count() >= 1) { if ($invoice->invitations->count() >= 1) {
@ -790,13 +795,18 @@ class InvoiceController extends BaseController
public function downloadPdf($invitation_key) public function downloadPdf($invitation_key)
{ {
$invitation = $this->invoice_repo->getInvitationByKey($invitation_key); $invitation = $this->invoice_repo->getInvitationByKey($invitation_key);
if(!$invitation)
return response()->json(["message" => "no record found"], 400);
$contact = $invitation->contact; $contact = $invitation->contact;
$invoice = $invitation->invoice; $invoice = $invitation->invoice;
$file = $invoice->service()->getInvoicePdf($contact); $file = $invoice->service()->getInvoicePdf($contact);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true); return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
} }
/** /**
@ -848,7 +858,9 @@ class InvoiceController extends BaseController
$file = $invoice->service()->getInvoiceDeliveryNote($invoice, $invoice->invitations->first()->contact); $file = $invoice->service()->getInvoiceDeliveryNote($invoice, $invoice->invitations->first()->contact);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true); return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
} }

View File

@ -16,6 +16,7 @@ use App\Utils\CurlUtils;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use stdClass; use stdClass;
use Carbon\Carbon;
class LicenseController extends BaseController class LicenseController extends BaseController
{ {
@ -152,7 +153,7 @@ class LicenseController extends BaseController
{ {
$account = auth()->user()->company()->account; $account = auth()->user()->company()->account;
if($account->plan == 'white_label' && $account->plan_expires->lt(now())){ if($account->plan == 'white_label' && Carbon::parse($account->plan_expires)->lt(now())){
$account->plan = null; $account->plan = null;
$account->plan_expires = null; $account->plan_expires = null;
$account->save(); $account->save();

View File

@ -82,6 +82,9 @@ class MigrationController extends BaseController
*/ */
public function purgeCompany(Company $company) public function purgeCompany(Company $company)
{ {
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$account = $company->account; $account = $company->account;
$company_id = $company->id; $company_id = $company->id;
@ -102,6 +105,9 @@ class MigrationController extends BaseController
private function purgeCompanyWithForceFlag(Company $company) private function purgeCompanyWithForceFlag(Company $company)
{ {
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$account = $company->account; $account = $company->account;
$company_id = $company->id; $company_id = $company->id;
@ -387,7 +393,6 @@ class MigrationController extends BaseController
else else
StartMigration::dispatch($migration_file, $user, $fresh_company); StartMigration::dispatch($migration_file, $user, $fresh_company);
} }
} }

View File

@ -208,9 +208,6 @@ class PaymentController extends BaseController
{ {
$payment = $this->payment_repo->save($request->all(), PaymentFactory::create(auth()->user()->company()->id, auth()->user()->id)); $payment = $this->payment_repo->save($request->all(), PaymentFactory::create(auth()->user()->company()->id, auth()->user()->id));
if($request->has('email_receipt') && $request->input('email_receipt') == 'true' && !$payment->client->getSetting('client_manual_payment_notification'))
$payment->service()->sendEmail();
return $this->itemResponse($payment); return $this->itemResponse($payment);
} }

View File

@ -0,0 +1,36 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use App\Http\Requests\Payments\PaymentNotificationWebhookRequest;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Utils\Traits\MakesHash;
use Auth;
class PaymentNotificationWebhookController extends Controller
{
use MakesHash;
public function __invoke(PaymentNotificationWebhookRequest $request, string $company_key, string $company_gateway_id, string $client_hash)
{
$company_gateway = CompanyGateway::find($this->decodePrimaryKey($company_gateway_id));
$client = Client::find($this->decodePrimaryKey($client_hash));
return $company_gateway
->driver($client)
->processWebhookRequest($request);
}
}

View File

@ -13,29 +13,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\Payments\PaymentWebhookRequest; use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Libraries\MultiDB;
use Auth;
class PaymentWebhookController extends Controller class PaymentWebhookController extends Controller
{ {
public function __invoke(PaymentWebhookRequest $request, string $company_key, string $company_gateway_id) public function __invoke(PaymentWebhookRequest $request)
{ {
return $request
// MultiDB::findAndSetDbByCompanyKey($company_key); ->getCompanyGateway()
->driver()
$payment = $request->getPayment(); ->processWebhookRequest($request);
if(!$payment)
return response()->json(['message' => 'Payment record not found.'], 400);
$client = is_null($payment) ? $request->getClient() : $payment->client;
if(!$client)
return response()->json(['message' => 'Client record not found.'], 400);
return $request->getCompanyGateway()
->driver($client)
->processWebhookRequest($request, $payment);
} }
} }

View File

@ -12,7 +12,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\DataMapper\Analytics\EmailBounce; use App\DataMapper\Analytics\EmailBounce;
use App\DataMapper\Analytics\EmailSpam; use App\DataMapper\Analytics\Mail\EmailSpam;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\CreditInvitation; use App\Models\CreditInvitation;

View File

@ -11,11 +11,28 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\DataMapper\Analytics\LivePreview;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
use App\Factory\QuoteFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Preview\PreviewInvoiceRequest;
use App\Jobs\Util\PreviewPdf; use App\Jobs\Util\PreviewPdf;
use App\Libraries\MultiDB;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Repositories\CreditRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\QuoteRepository;
use App\Repositories\RecurringInvoiceRepository;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\Design; use App\Services\PdfMaker\Design;
use App\Services\PdfMaker\PdfMaker; use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf; use App\Utils\HostedPDF\NinjaPdf;
@ -28,6 +45,7 @@ use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Lang; use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Turbo124\Beacon\Facades\LightLogs;
class PreviewController extends BaseController class PreviewController extends BaseController
{ {
@ -118,6 +136,7 @@ class PreviewController extends BaseController
'products' => request()->design['design']['product'], 'products' => request()->design['design']['product'],
]), ]),
'variables' => $html->generateLabelsAndValues(), 'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $entity_obj->client->company->markdown_enabled,
]; ];
$design = new Design(request()->design['name']); $design = new Design(request()->design['name']);
@ -149,13 +168,150 @@ class PreviewController extends BaseController
return $this->blankEntity(); return $this->blankEntity();
} }
public function live(PreviewInvoiceRequest $request)
{
$company = auth()->user()->company();
MultiDB::setDb($company->db);
if($request->input('entity') == 'invoice'){
$repo = new InvoiceRepository();
$entity_obj = InvoiceFactory::create($company->id, auth()->user()->id);
$class = Invoice::class;
}
elseif($request->input('entity') == 'quote'){
$repo = new QuoteRepository();
$entity_obj = QuoteFactory::create($company->id, auth()->user()->id);
$class = Quote::class;
}
elseif($request->input('entity') == 'credit'){
$repo = new CreditRepository();
$entity_obj = CreditFactory::create($company->id, auth()->user()->id);
$class = Credit::class;
}
elseif($request->input('entity') == 'recurring_invoice'){
$repo = new RecurringInvoiceRepository();
$entity_obj = RecurringInvoiceFactory::create($company->id, auth()->user()->id);
$class = RecurringInvoice::class;
}
try {
DB::connection(config('database.default'))->beginTransaction();
if($request->has('entity_id')){
$entity_obj = $class::on(config('database.default'))
->where('id', $this->decodePrimaryKey($request->input('entity_id')))
->where('company_id', $company->id)
->withTrashed()
->first();
}
$entity_obj = $repo->save($request->all(), $entity_obj);
$entity_obj->load('client');
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($entity_obj->client->contacts()->first()->preferredLocale());
$t->replace(Ninja::transformTranslations($entity_obj->client->getMergedSettings()));
$html = new HtmlEngine($entity_obj->invitations()->first());
$design = \App\Models\Design::find($entity_obj->design_id);
/* Catch all in case migration doesn't pass back a valid design */
if(!$design)
$design = \App\Models\Design::find(2);
if ($design->is_custom) {
$options = [
'custom_partials' => json_decode(json_encode($design->design), true)
];
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
} else {
$template = new PdfMakerDesign(strtolower($design->name));
}
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => $entity_obj->client,
'entity' => $entity_obj,
'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables,
'$product' => $design->design->product,
'variables' => $variables,
]),
'variables' => $variables,
'options' => [
'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'),
'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'),
],
'process_markdown' => $entity_obj->client->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($template)
->build();
DB::connection(config('database.default'))->rollBack();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML;
}
}
catch(\Exception $e){
DB::connection(config('database.default'))->rollBack();
return;
}
//if phantom js...... inject here..
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
return (new NinjaPdf())->build($maker->getCompiledHTML(true));
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), $company);
if(Ninja::isHosted())
{
LightLogs::create(new LivePreview())
->increment()
->batch();
}
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
private function blankEntity() private function blankEntity()
{ {
App::forgetInstance('translator'); App::forgetInstance('translator');
$t = app('translator'); $t = app('translator');
$t->replace(Ninja::transformTranslations(auth()->user()->company()->settings)); $t->replace(Ninja::transformTranslations(auth()->user()->company()->settings));
DB::beginTransaction(); DB::connection(auth()->user()->company()->db)->beginTransaction();
$client = Client::factory()->create([ $client = Client::factory()->create([
'user_id' => auth()->user()->id, 'user_id' => auth()->user()->id,
@ -208,6 +364,7 @@ class PreviewController extends BaseController
'products' => request()->design['design']['product'], 'products' => request()->design['design']['product'],
]), ]),
'variables' => $html->generateLabelsAndValues(), 'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $invoice->client->company->markdown_enabled,
]; ];
$maker = new PdfMaker($state); $maker = new PdfMaker($state);
@ -230,7 +387,7 @@ class PreviewController extends BaseController
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company()); $file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
DB::rollBack(); DB::connection(auth()->user()->company()->db)->rollBack();
$response = Response::make($file_path, 200); $response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf'); $response->header('Content-Type', 'application/pdf');

View File

@ -470,7 +470,7 @@ class ProductController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$products = Product::withTrashed()->find($this->transformKeys($ids)); $products = Product::withTrashed()->whereIn('id', $this->transformKeys($ids))->cursor();
$products->each(function ($product, $key) use ($action) { $products->each(function ($product, $key) use ($action) {
if (auth()->user()->can('edit', $product)) { if (auth()->user()->can('edit', $product)) {

View File

@ -39,6 +39,7 @@ use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments; use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/** /**
* Class QuoteController. * Class QuoteController.
@ -676,8 +677,14 @@ class QuoteController extends BaseController
break; break;
case 'download': case 'download':
$file = $quote->pdf_file_path(); //$file = $quote->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true); $file = $quote->service()->getQuotePdf();
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
//return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
break; break;
case 'restore': case 'restore':
@ -728,9 +735,13 @@ class QuoteController extends BaseController
$contact = $invitation->contact; $contact = $invitation->contact;
$quote = $invitation->quote; $quote = $invitation->quote;
$file_path = $quote->service()->getQuotePdf($contact); $file = $quote->service()->getQuotePdf($contact);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true); return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
// return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
} }
/** /**

View File

@ -33,6 +33,7 @@ use App\Utils\Traits\SavesDocuments;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/** /**
* Class RecurringInvoiceController. * Class RecurringInvoiceController.
@ -500,9 +501,12 @@ class RecurringInvoiceController extends BaseController
$contact = $invitation->contact; $contact = $invitation->contact;
$recurring_invoice = $invitation->recurring_invoice; $recurring_invoice = $invitation->recurring_invoice;
$file_path = $recurring_invoice->service()->getInvoicePdf($contact); $file = $recurring_invoice->service()->getInvoicePdf($contact);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
} }
/** /**

View File

@ -16,6 +16,7 @@ use App\Http\Requests\Setup\CheckDatabaseRequest;
use App\Http\Requests\Setup\CheckMailRequest; use App\Http\Requests\Setup\CheckMailRequest;
use App\Http\Requests\Setup\StoreSetupRequest; use App\Http\Requests\Setup\StoreSetupRequest;
use App\Jobs\Account\CreateAccount; use App\Jobs\Account\CreateAccount;
use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\VersionCheck; use App\Jobs\Util\VersionCheck;
use App\Models\Account; use App\Models\Account;
use App\Utils\CurlUtils; use App\Utils\CurlUtils;
@ -69,8 +70,6 @@ class SetupController extends Controller
} }
if ($check['system_health'] === false) { if ($check['system_health'] === false) {
nlog($check);
return response('Oops, something went wrong. Check your logs.'); /* We should never reach this block, but just in case. */ return response('Oops, something went wrong. Check your logs.'); /* We should never reach this block, but just in case. */
} }
@ -109,11 +108,11 @@ class SetupController extends Controller
'REQUIRE_HTTPS' => $request->input('https') ? 'true' : 'false', 'REQUIRE_HTTPS' => $request->input('https') ? 'true' : 'false',
'APP_DEBUG' => 'false', 'APP_DEBUG' => 'false',
'DB_HOST1' => $request->input('db_host'), 'DB_HOST' => $request->input('db_host'),
'DB_PORT1' => $request->input('db_port'), 'DB_PORT' => $request->input('db_port'),
'DB_DATABASE1' => $request->input('db_database'), 'DB_DATABASE' => $request->input('db_database'),
'DB_USERNAME1' => $request->input('db_username'), 'DB_USERNAME' => $request->input('db_username'),
'DB_PASSWORD1' => $request->input('db_password'), 'DB_PASSWORD' => $request->input('db_password'),
'MAIL_MAILER' => $mail_driver, 'MAIL_MAILER' => $mail_driver,
'MAIL_PORT' => $request->input('mail_port'), 'MAIL_PORT' => $request->input('mail_port'),
@ -125,6 +124,7 @@ class SetupController extends Controller
'MAIL_PASSWORD' => $request->input('mail_password'), 'MAIL_PASSWORD' => $request->input('mail_password'),
'NINJA_ENVIRONMENT' => 'selfhost', 'NINJA_ENVIRONMENT' => 'selfhost',
'DB_CONNECTION' => 'mysql',
]; ];
if (config('ninja.db.multi_db_enabled')) { if (config('ninja.db.multi_db_enabled')) {
@ -133,11 +133,11 @@ class SetupController extends Controller
if (config('ninja.preconfigured_install')) { if (config('ninja.preconfigured_install')) {
// Database connection was already configured. Don't let the user override it. // Database connection was already configured. Don't let the user override it.
unset($env_values['DB_HOST1']); unset($env_values['DB_HOST']);
unset($env_values['DB_PORT1']); unset($env_values['DB_PORT']);
unset($env_values['DB_DATABASE1']); unset($env_values['DB_DATABASE']);
unset($env_values['DB_USERNAME1']); unset($env_values['DB_USERNAME']);
unset($env_values['DB_PASSWORD1']); unset($env_values['DB_PASSWORD']);
} }
try { try {
@ -240,6 +240,11 @@ class SetupController extends Controller
$pdf->setChromiumPath(config('ninja.snappdf_chromium_path')); $pdf->setChromiumPath(config('ninja.snappdf_chromium_path'));
} }
if (config('ninja.snappdf_chromium_arguments')) {
$pdf->clearChromiumArguments();
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
}
$pdf = $pdf $pdf = $pdf
->setHtml('GENERATING PDFs WORKS! Thank you for using Invoice Ninja!') ->setHtml('GENERATING PDFs WORKS! Thank you for using Invoice Ninja!')
->generate(); ->generate();
@ -275,10 +280,7 @@ class SetupController extends Controller
public function update() public function update()
{ {
// if(Ninja::isHosted())
// return redirect('/');
// if( Ninja::isNinja() || !request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) )
if(!request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) ) if(!request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) )
return redirect('/'); return redirect('/');
@ -307,6 +309,8 @@ class SetupController extends Controller
$this->buildCache(true); $this->buildCache(true);
SchedulerCheck::dispatchNow();
return redirect('/'); return redirect('/');
} }

View File

@ -1,5 +1,4 @@
<?php <?php
/** /**
* Invoice Ninja (https://invoiceninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
@ -52,7 +51,7 @@ class StripeConnectController extends BaseController
$config = $company_gateway->getConfig(); $config = $company_gateway->getConfig();
if(property_exists($config, 'account_id')) if(property_exists($config, 'account_id') && strlen($config->account_id) > 1)
return view('auth.connect.existing'); return view('auth.connect.existing');
} }
@ -61,12 +60,6 @@ class StripeConnectController extends BaseController
$redirect_uri = 'https://invoicing.co/stripe/completed'; $redirect_uri = 'https://invoicing.co/stripe/completed';
$endpoint = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id={$stripe_client_id}&redirect_uri={$redirect_uri}&scope=read_write&state={$token}"; $endpoint = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id={$stripe_client_id}&redirect_uri={$redirect_uri}&scope=read_write&state={$token}";
// if($email = $request->getContact()->email)
// $endpoint .= "&stripe_user[email]={$email}";
// $company_name = str_replace(" ", "_", $company->present()->name());
// $endpoint .= "&stripe_user[business_name]={$company_name}";
return redirect($endpoint); return redirect($endpoint);
} }
@ -87,18 +80,27 @@ class StripeConnectController extends BaseController
nlog($e->getMessage()); nlog($e->getMessage());
} }
// nlog($response); MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = Company::where('company_key', $request->getTokenContent()['company_key'])->first(); $company = Company::where('company_key', $request->getTokenContent()['company_key'])->first();
$company_gateway = CompanyGatewayFactory::create($company->id, $company->owner()->id); $company_gateway = CompanyGateway::query()
$fees_and_limits = new \stdClass; ->where('gateway_key', 'd14dd26a47cecc30fdd65700bfb67b34')
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits; ->where('company_id', $company->id)
$company_gateway->gateway_key = 'd14dd26a47cecc30fdd65700bfb67b34'; ->first();
$company_gateway->fees_and_limits = $fees_and_limits;
$company_gateway->setConfig([]); if(!$company_gateway)
// $company_gateway->save(); {
$company_gateway = CompanyGatewayFactory::create($company->id, $company->owner()->id);
$fees_and_limits = new \stdClass;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits;
$company_gateway->gateway_key = 'd14dd26a47cecc30fdd65700bfb67b34';
$company_gateway->fees_and_limits = $fees_and_limits;
$company_gateway->setConfig([]);
$company_gateway->token_billing = 'always';
// $company_gateway->save();
}
$payload = [ $payload = [
'account_id' => $response->stripe_user_id, 'account_id' => $response->stripe_user_id,
@ -111,18 +113,6 @@ class StripeConnectController extends BaseController
"access_token" => $response->access_token "access_token" => $response->access_token
]; ];
/* Link account if existing account exists */
// if($account_id = $this->checkAccountAlreadyLinkToEmail($company_gateway, $request->getContact()->email)) {
// $payload['account_id'] = $account_id;
// $payload['stripe_user_id'] = $account_id;
// $company_gateway->setConfig($payload);
// $company_gateway->save();
// return view('auth.connect.existing');
// }
$company_gateway->setConfig($payload); $company_gateway->setConfig($payload);
$company_gateway->save(); $company_gateway->save();

View File

@ -14,10 +14,14 @@ namespace App\Http\Controllers;
use App\Jobs\Util\ImportStripeCustomers; use App\Jobs\Util\ImportStripeCustomers;
use App\Jobs\Util\StripeUpdatePaymentMethods; use App\Jobs\Util\StripeUpdatePaymentMethods;
use App\Models\Client;
use App\Models\CompanyGateway;
class StripeController extends BaseController class StripeController extends BaseController
{ {
private $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23'];
public function update() public function update()
{ {
if(auth()->user()->isAdmin()) if(auth()->user()->isAdmin())
@ -29,13 +33,15 @@ class StripeController extends BaseController
} }
return response()->json(['message' => 'Unauthorized'], 403); return response()->json(['message' => 'Unauthorized'], 403);
} }
public function import() public function import()
{ {
// return response()->json(['message' => 'Processing'], 200);
if(auth()->user()->isAdmin()) if(auth()->user()->isAdmin())
{ {
@ -44,9 +50,26 @@ class StripeController extends BaseController
return response()->json(['message' => 'Processing'], 200); return response()->json(['message' => 'Processing'], 200);
} }
return response()->json(['message' => 'Unauthorized'], 403); return response()->json(['message' => 'Unauthorized'], 403);
} }
public function verify()
{
if(auth()->user()->isAdmin())
{
$company_gateway = CompanyGateway::where('company_id', auth()->user()->company()->id)
->where('is_deleted',0)
->whereIn('gateway_key', $this->stripe_keys)
->first();
return $company_gateway->driver(new Client)->verifyConnect();
}
return response()->json(['message' => 'Unauthorized'], 403);
}
} }

View File

@ -26,6 +26,9 @@ class SubdomainController extends BaseController
'docs', 'docs',
'client_domain', 'client_domain',
'custom_domain', 'custom_domain',
'preview',
'invoiceninja',
'cname',
]; ];
public function __construct() public function __construct()

View File

@ -354,6 +354,8 @@ class SubscriptionController extends BaseController
event(new SubscriptionWasUpdated($subscription, $subscription->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new SubscriptionWasUpdated($subscription, $subscription->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
nlog($subscription->id);
return $this->itemResponse($subscription); return $this->itemResponse($subscription);
} }

View File

@ -76,7 +76,7 @@ class SendingController extends Controller
} }
Mail::to(config('ninja.contact.ninja_official_contact')) Mail::to(config('ninja.contact.ninja_official_contact'))
->send(new SupportMessageSent($request->message, $send_logs)); ->send(new SupportMessageSent($request->input('message'), $send_logs));
return response()->json([ return response()->json([
'success' => true, 'success' => true,

View File

@ -11,11 +11,17 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use PragmaRX\Google2FA\Google2FA; use App\Models\User;
use App\Transformers\UserTransformer;
use Crypt; use Crypt;
use PragmaRX\Google2FA\Google2FA;
class TwoFactorController extends BaseController class TwoFactorController extends BaseController
{ {
protected $entity_type = User::class;
protected $entity_transformer = UserTransformer::class;
public function setupTwoFactor() public function setupTwoFactor()
{ {
$user = auth()->user(); $user = auth()->user();

View File

@ -0,0 +1,54 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\User;
use App\PaymentDrivers\WePayPaymentDriver;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class WePayController extends BaseController
{
use MakesHash;
/**
* Initialize WePay Signup.
*/
public function signup(string $token)
{
$hash = Cache::get($token);
MultiDB::findAndSetDbByCompanyKey($hash['company_key']);
$user = User::findOrFail($hash['user_id']);
$company = Company::where('company_key', $hash['company_key'])->firstOrFail();
$data['user_id'] = $user->id;
$data['company'] = $company;
$wepay_driver = new WePayPaymentDriver(new CompanyGateway, null, null);
return $wepay_driver->setup($data);
}
public function finished()
{
return render('gateways.wepay.signup.finished');
}
}

View File

@ -30,6 +30,7 @@ use App\Http\Middleware\QueryLogging;
use App\Http\Middleware\RedirectIfAuthenticated; use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\SetDb; use App\Http\Middleware\SetDb;
use App\Http\Middleware\SetDbByCompanyKey; use App\Http\Middleware\SetDbByCompanyKey;
use App\Http\Middleware\SetDocumentDb;
use App\Http\Middleware\SetDomainNameDb; use App\Http\Middleware\SetDomainNameDb;
use App\Http\Middleware\SetEmailDb; use App\Http\Middleware\SetEmailDb;
use App\Http\Middleware\SetInviteDb; use App\Http\Middleware\SetInviteDb;
@ -158,6 +159,7 @@ class Kernel extends HttpKernel
'contact_key_login' => ContactKeyLogin::class, 'contact_key_login' => ContactKeyLogin::class,
'check_client_existence' => CheckClientExistence::class, 'check_client_existence' => CheckClientExistence::class,
'user_verified' => UserVerified::class, 'user_verified' => UserVerified::class,
'document_db' => SetDocumentDb::class,
]; ];

View File

@ -237,7 +237,7 @@ class BillingPortalPurchase extends Component
$client_repo = new ClientRepository(new ClientContactRepository()); $client_repo = new ClientRepository(new ClientContactRepository());
$data = [ $data = [
'name' => 'Client Name', 'name' => '',
'contacts' => [ 'contacts' => [
['email' => $this->email], ['email' => $this->email],
], ],

View File

@ -26,7 +26,7 @@ class CreditsTable extends Component
public $per_page = 10; public $per_page = 10;
public $company; public $company;
public function mount() public function mount()
{ {
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
@ -34,9 +34,15 @@ class CreditsTable extends Component
public function render() public function render()
{ {
$query = Credit::query() $query = Credit::query()
->where('client_id', auth('contact')->user()->client->id) ->where('client_id', auth('contact')->user()->client->id)
->where('company_id', $this->company->id)
->where('status_id', '<>', Credit::STATUS_DRAFT) ->where('status_id', '<>', Credit::STATUS_DRAFT)
->where(function ($query){
$query->whereDate('due_date', '<=', now())
->orWhereNull('due_date');
})
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -44,6 +44,7 @@ class InvoicesTable extends Component
$query = Invoice::query() $query = Invoice::query()
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->where('company_id', $this->company->id)
->where('is_deleted', false); ->where('is_deleted', false);
if (in_array('paid', $this->status)) { if (in_array('paid', $this->status)) {

View File

@ -0,0 +1,56 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Livewire\PaymentMethods;
use App\Libraries\MultiDB;
use Livewire\Component;
class UpdateDefaultMethod extends Component
{
/** @var \App\Models\Company */
public $company;
/** @var \App\Models\ClientGatewayToken */
public $token;
/** @var \App\Models\Client */
public $client;
public function mount()
{
$this->company = $this->client->company;
MultiDB::setDb($this->company->db);
$this->is_disabled = $this->token->is_default;
}
public function makeDefault(): void
{
if ($this->token->is_default) {
return;
}
$this->client->gateway_tokens()->update(['is_default' => 0]);
$this->token->is_default = 1;
$this->token->save();
$this->emit('UpdateDefaultMethod::method-updated');
}
public function render()
{
return render('components.livewire.update-default-payment-method');
}
}

View File

@ -34,6 +34,7 @@ class PaymentMethodsTable extends Component
{ {
$query = ClientGatewayToken::query() $query = ClientGatewayToken::query()
->with('gateway_type') ->with('gateway_type')
->where('company_id', $this->company->id)
->where('client_id', $this->client->id) ->where('client_id', $this->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -41,6 +41,7 @@ class PaymentsTable extends Component
{ {
$query = Payment::query() $query = Payment::query()
->with('type', 'client') ->with('type', 'client')
->where('company_id', $this->company->id)
->where('client_id', auth('contact')->user()->client->id) ->where('client_id', auth('contact')->user()->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -32,6 +32,7 @@ class General extends Component
'first_name' => ['sometimes'], 'first_name' => ['sometimes'],
'last_name' => ['sometimes'], 'last_name' => ['sometimes'],
'email' => ['required', 'email'], 'email' => ['required', 'email'],
'phone' => ['sometimes'],
]; ];
public function mount() public function mount()

View File

@ -45,6 +45,7 @@ class QuotesTable extends Component
} }
$query = $query $query = $query
->where('company_id', $this->company->id)
->where('client_id', auth('contact')->user()->client->id) ->where('client_id', auth('contact')->user()->client->id)
->where('status_id', '<>', Quote::STATUS_DRAFT) ->where('status_id', '<>', Quote::STATUS_DRAFT)
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -0,0 +1,34 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Livewire\RecurringInvoices;
use Livewire\Component;
class UpdateAutoBilling extends Component
{
/** @var \App\Models\RecurringInvoice */
public $invoice;
public function updateAutoBilling(): void
{
if ($this->invoice->auto_bill === 'optin' || $this->invoice->auto_bill === 'optout') {
$this->invoice->auto_bill_enabled = !$this->invoice->auto_bill_enabled;
$this->invoice->save();
}
}
public function render()
{
return render('components.livewire.recurring-invoices-switch-autobilling');
}
}

View File

@ -12,6 +12,7 @@
namespace App\Http\Livewire; namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Utils\Traits\WithSorting; use App\Utils\Traits\WithSorting;
use Livewire\Component; use Livewire\Component;
@ -23,8 +24,12 @@ class RecurringInvoicesTable extends Component
public $per_page = 10; public $per_page = 10;
public $company;
public function mount() public function mount()
{ {
MultiDB::setDb($this->company->db);
$this->sort_asc = false; $this->sort_asc = false;
$this->sort_field = 'date'; $this->sort_field = 'date';
@ -36,6 +41,7 @@ class RecurringInvoicesTable extends Component
$query = $query $query = $query
->where('client_id', auth('contact')->user()->client->id) ->where('client_id', auth('contact')->user()->client->id)
->where('company_id', $this->company->id)
->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_PAUSED,RecurringInvoice::STATUS_COMPLETED]) ->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_PAUSED,RecurringInvoice::STATUS_COMPLETED])
->orderBy('status_id', 'asc') ->orderBy('status_id', 'asc')
->with('client') ->with('client')

View File

@ -158,6 +158,37 @@ class RequiredClientInfo extends Component
} }
} }
public function showCopyBillingCheckbox(): bool
{
$fields = [];
collect($this->fields)->map(function ($field) use (&$fields) {
if (! array_key_exists('filled', $field)) {
$fields[] = $field['name'];
}
});
foreach ($fields as $field) {
if (Str::startsWith($field, 'client_shipping')) {
return true;
}
}
return false;
}
public function handleCopyBilling(): void
{
$this->emit('update-shipping-data', [
'client_shipping_address_line_1' => $this->contact->client->address1,
'client_shipping_address_line_2' => $this->contact->client->address2,
'client_shipping_city' => $this->contact->client->city,
'client_shipping_state' => $this->contact->client->state,
'client_shipping_postal_code' => $this->contact->client->postal_code,
'client_shipping_country_id' => $this->contact->client->country_id,
]);
}
public function render() public function render()
{ {
count($this->fields) > 0 count($this->fields) > 0

View File

@ -90,24 +90,40 @@ class SubscriptionPlanSwitch extends Component
$this->state['show_loading_bar'] = true; $this->state['show_loading_bar'] = true;
$this->state['invoice'] = $this->target->service()->createChangePlanInvoice([ $payment_required = $this->target->service()->changePlanPaymentCheck([
'recurring_invoice' => $this->recurring_invoice, 'recurring_invoice' => $this->recurring_invoice,
'subscription' => $this->subscription, 'subscription' => $this->subscription,
'target' => $this->target, 'target' => $this->target,
'hash' => $this->hash, 'hash' => $this->hash,
]); ]);
Cache::put($this->hash, [ if($payment_required)
'subscription_id' => $this->target->id, {
'target_id' => $this->target->id,
'recurring_invoice' => $this->recurring_invoice->id, $this->state['invoice'] = $this->target->service()->createChangePlanInvoice([
'client_id' => $this->recurring_invoice->client->id, 'recurring_invoice' => $this->recurring_invoice,
'invoice_id' => $this->state['invoice']->id, 'subscription' => $this->subscription,
'context' => 'change_plan', 'target' => $this->target,
now()->addMinutes(60)] 'hash' => $this->hash,
); ]);
Cache::put($this->hash, [
'subscription_id' => $this->target->id,
'target_id' => $this->target->id,
'recurring_invoice' => $this->recurring_invoice->id,
'client_id' => $this->recurring_invoice->client->id,
'invoice_id' => $this->state['invoice']->id,
'context' => 'change_plan',
now()->addMinutes(60)]
);
$this->state['payment_initialised'] = true;
}
else
$this->handlePaymentNotRequired();
$this->state['payment_initialised'] = true;
$this->emit('beforePaymentEventsCompleted'); $this->emit('beforePaymentEventsCompleted');
} }

View File

@ -36,6 +36,7 @@ class SubscriptionRecurringInvoicesTable extends Component
{ {
$query = RecurringInvoice::query() $query = RecurringInvoice::query()
->where('client_id', auth('contact')->user()->client->id) ->where('client_id', auth('contact')->user()->client->id)
->where('company_id', $this->company->id)
->whereNotNull('subscription_id') ->whereNotNull('subscription_id')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -26,17 +26,27 @@ class TasksTable extends Component
public $per_page = 10; public $per_page = 10;
public $company; public $company;
public function mount() public function mount()
{ {
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
} }
public function render() public function render()
{ {
$query = Task::query() $query = Task::query()
->where('client_id', auth('contact')->user()->client->id) ->where('company_id', $this->company->id)
->whereNotNull('invoice_id') ->where('client_id', auth('contact')->user()->client->id);
if ($this->company->getSetting('show_all_tasks_client_portal') === 'invoiced') {
$query = $query->whereNotNull('invoice_id');
}
if ($this->company->getSetting('show_all_tasks_client_portal') === 'uninvoiced') {
$query = $query->whereNull('invoice_id');
}
$query = $query
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -0,0 +1,201 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Livewire;
use App\DataMapper\FeesAndLimits;
use App\Factory\CompanyGatewayFactory;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\User;
use App\PaymentDrivers\WePayPaymentDriver;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use WePay;
class WepaySignup extends Component
{
public $user;
public $user_id;
public $company_key;
public $first_name;
public $last_name;
public $email;
public $company_name;
public $country;
public $ach;
public $wepay_payment_tos_agree;
public $debit_cards;
public $terms;
public $privacy_policy;
public $saved;
public $company;
protected $rules = [
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email'],
'company_name' => ['required'],
'country' => ['required'],
'ach' => ['sometimes'],
'wepay_payment_tos_agree' => ['accepted'],
'debit_cards' => ['sometimes'],
];
public function mount()
{
MultiDB::setDb($this->company->db);
$user = User::find($this->user_id);
$this->company = Company::where('company_key', $this->company->company_key)->first();
$this->fill([
'wepay_payment_tos_agree' => '',
'ach' => '',
'country' => 'US',
'user' => $user,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->email,
'company_name' => $this->company->present()->name(),
'saved' => ctrans('texts.confirm'),
'terms' => '<a href="https://go.wepay.com/terms-of-service" target="_blank">'.ctrans('texts.terms_of_service').'</a>',
'privacy_policy' => '<a href="https://go.wepay.com/privacy-policy" target="_blank">'.ctrans('texts.privacy_policy').'</a>',
]);
}
public function render()
{
return render('gateways.wepay.signup.wepay-signup');
}
public function submit()
{
$data = $this->validate($this->rules);
//need to create or get a new WePay CompanyGateway
$cg = CompanyGateway::where('gateway_key', '8fdeed552015b3c7b44ed6c8ebd9e992')
->where('company_id', $this->company->id)
->firstOrNew();
if(!$cg->id) {
$fees_and_limits = new \stdClass;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits;
$fees_and_limits->{GatewayType::BANK_TRANSFER} = new FeesAndLimits;
$cg = CompanyGatewayFactory::create($this->company->id, $this->user->id);
$cg->gateway_key = '8fdeed552015b3c7b44ed6c8ebd9e992';
$cg->require_cvv = false;
$cg->require_billing_address = false;
$cg->require_shipping_address = false;
$cg->update_details = false;
$cg->config = encrypt(config('ninja.testvars.checkout'));
$cg->fees_and_limits = $fees_and_limits;
$cg->token_billing = 'always';
$cg->save();
}
$this->saved = ctrans('texts.processing');
$wepay_driver = new WePayPaymentDriver($cg, null, null);
$wepay = $wepay_driver->init()->wepay;
$user_details = [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'email' => $data['email'],
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'original_ip' => request()->ip(),
'original_device' => request()->server('HTTP_USER_AGENT'),
'tos_acceptance_time' => time(),
'redirect_uri' => route('wepay.finished'),
'scope' => 'manage_accounts,collect_payments,view_user,preapprove_payments,send_money',
];
$wepay_user = $wepay->request('user/register/', $user_details);
$access_token = $wepay_user->access_token;
$access_token_expires = $wepay_user->expires_in ? (time() + $wepay_user->expires_in) : null;
$wepay = new WePay($access_token);
$account_details = [
'name' => $data['company_name'],
'description' => ctrans('texts.wepay_account_description'),
'theme_object' => json_decode('{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}'),
'callback_uri' => route('payment_webhook', ['company_key' => $this->company->company_key, 'company_gateway_id' => $cg->hashed_id]),
'rbits' => $this->company->rBits(),
'country' => $data['country'],
];
if ($data['country'] == 'CA') {
$account_details['currencies'] = ['CAD'];
$account_details['country_options'] = ['debit_opt_in' => boolval($data['debit_cards'])];
} elseif ($data['country'] == 'GB') {
$account_details['currencies'] = ['GBP'];
}
$wepay_account = $wepay->request('account/create/', $account_details);
try {
$wepay->request('user/send_confirmation/', []);
$confirmation_required = true;
} catch (\WePayException $ex) {
if ($ex->getMessage() == 'This access_token is already approved.') {
$confirmation_required = false;
} else {
request()->session()->flash('message', $ex->getMessage());
}
nlog("failed in try catch ");
nlog($ex->getMessage());
}
$config = [
'userId' => $wepay_user->user_id,
'accessToken' => $access_token,
'tokenType' => $wepay_user->token_type,
'tokenExpires' => $access_token_expires,
'accountId' => $wepay_account->account_id,
'state' => $wepay_account->state,
'testMode' => config('ninja.wepay.environment') == 'staging',
'country' => $data['country'],
];
$cg->setConfig($config);
$cg->save();
if ($confirmation_required) {
request()->session()->flash('message', trans('texts.created_wepay_confirmation_required'));
} else {
$update_uri = $wepay->request('/account/get_update_uri', [
'account_id' => $wepay_account->account_id,
'redirect_uri' => config('ninja.app_url'),
]);
return redirect($update_uri->uri);
}
return redirect()->to('/wepay/finished');
}
}

View File

@ -29,6 +29,7 @@ class CheckClientExistence
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
$multiple_contacts = ClientContact::query() $multiple_contacts = ClientContact::query()
->with('company','client')
->where('email', auth('contact')->user()->email) ->where('email', auth('contact')->user()->email)
->whereNotNull('email') ->whereNotNull('email')
->where('email', '<>', '') ->where('email', '<>', '')

View File

@ -71,11 +71,16 @@ class ContactKeyLogin
} }
} elseif ($request->segment(2) && $request->segment(2) == 'key_login' && $request->segment(3)) { } elseif ($request->segment(2) && $request->segment(2) == 'key_login' && $request->segment(3)) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) { if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if(empty($client_contact->email)) {
if(empty($client_contact->email))
$client_contact->email = Str::random(6) . "@example.com"; $client_contact->save(); $client_contact->email = Str::random(6) . "@example.com"; $client_contact->save();
}
auth()->guard('contact')->login($client_contact, true); auth()->guard('contact')->login($client_contact, true);
if ($request->query('next')) {
return redirect($request->query('next'));
}
return redirect()->to('client/dashboard'); return redirect()->to('client/dashboard');
} }
} elseif ($request->has('client_hash') && config('ninja.db.multi_db_enabled')) { } elseif ($request->has('client_hash') && config('ninja.db.multi_db_enabled')) {
@ -106,7 +111,6 @@ class ContactKeyLogin
} }
} }
return $next($request); return $next($request);
} }
} }

View File

@ -33,7 +33,8 @@ class ContactRegister
if($company) if($company)
{ {
abort_unless($company->client_can_register, 404); if(! $company->client_can_register)
abort(400, 'Registration disabled');
$request->merge(['key' => $company->company_key]); $request->merge(['key' => $company->company_key]);
@ -49,7 +50,9 @@ class ContactRegister
if($company = Company::where($query)->first()) if($company = Company::where($query)->first())
{ {
abort_unless($company->client_can_register, 404);
if(! $company->client_can_register)
abort(400, 'Registration disabled');
$request->merge(['key' => $company->company_key]); $request->merge(['key' => $company->company_key]);
@ -62,7 +65,10 @@ class ContactRegister
if ($request->route()->parameter('company_key') && Ninja::isSelfHost()) { if ($request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Company::where('company_key', $request->company_key)->firstOrFail(); $company = Company::where('company_key', $request->company_key)->firstOrFail();
abort_unless($company->client_can_register, 404); if(! (bool)$company->client_can_register);
abort(400, 'Registration disabled');
$request->merge(['key' => $company->company_key]);
return $next($request); return $next($request);
} }
@ -72,7 +78,8 @@ class ContactRegister
if (!$request->route()->parameter('company_key') && Ninja::isSelfHost()) { if (!$request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Account::first()->default_company; $company = Account::first()->default_company;
abort_unless($company->client_can_register, 404); if(! $company->client_can_register)
abort(400, 'Registration disabled');
$request->merge(['key' => $company->company_key]); $request->merge(['key' => $company->company_key]);

View File

@ -44,6 +44,14 @@ class PasswordProtection
else else
$timeout = $timeout/1000; $timeout = $timeout/1000;
//test if password if base64 encoded
$x_api_password = $request->header('X-API-PASSWORD');
if($request->header('X-API-PASSWORD-BASE64'))
{
$x_api_password = base64_decode($request->header('X-API-PASSWORD-BASE64'));
}
if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in')) { if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in')) {
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
@ -57,9 +65,6 @@ class PasswordProtection
$user = false; $user = false;
$google = new Google(); $google = new Google();
$user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD')); $user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD'));
nlog("user");
nlog($user);
if (is_array($user)) { if (is_array($user)) {
@ -68,10 +73,8 @@ class PasswordProtection
'oauth_provider_id'=> 'google' 'oauth_provider_id'=> 'google'
]; ];
nlog($query);
//If OAuth and user also has a password set - check both //If OAuth and user also has a password set - check both
if ($existing_user = MultiDB::hasUser($query) && auth()->user()->company()->oauth_password_required && auth()->user()->has_password && Hash::check(auth()->user()->password, $request->header('X-API-PASSWORD'))) { if ($existing_user = MultiDB::hasUser($query) && auth()->user()->company()->oauth_password_required && auth()->user()->has_password && Hash::check(auth()->user()->password, $x_api_password)) {
nlog("existing user with password"); nlog("existing user with password");
@ -91,7 +94,7 @@ class PasswordProtection
return response()->json($error, 412); return response()->json($error, 412);
}elseif ($request->header('X-API-PASSWORD') && Hash::check($request->header('X-API-PASSWORD'), auth()->user()->password)) { }elseif ($x_api_password && Hash::check($x_api_password, auth()->user()->password)) {
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);

View File

@ -51,12 +51,19 @@ class QueryLogging
$count = count($queries); $count = count($queries);
$timeEnd = microtime(true); $timeEnd = microtime(true);
$time = $timeEnd - $timeStart; $time = $timeEnd - $timeStart;
//nlog($request->method().' - '.urldecode($request->url()).": $count queries - ".$time); // if($count > 150)
// if($count > 50) // nlog($queries);
//nlog($queries);
$ip = '';
LightLogs::create(new DbQuery($request->method(), urldecode($request->url()), $count, $time, request()->ip())) if(request()->header('Cf-Connecting-Ip'))
$ip = request()->header('Cf-Connecting-Ip');
else{
$ip = request()->ip();
}
LightLogs::create(new DbQuery($request->method(), urldecode($request->url()), $count, $time, $ip))
->batch(); ->batch();
} }

View File

@ -0,0 +1,44 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Middleware;
use App\Libraries\MultiDB;
use Closure;
use Illuminate\Http\Request;
use stdClass;
class SetDocumentDb
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$error = [
'message' => 'Document not set or not found',
'errors' => new stdClass,
];
if (config('ninja.db.multi_db_enabled')) {
if (! MultiDB::documentFindAndSetDb($request->segment(2)))
return response()->json($error, 400);
}
return $next($request);
}
}

View File

@ -30,17 +30,25 @@ class UrlSetDb
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
if (config('ninja.db.multi_db_enabled')) { if (config('ninja.db.multi_db_enabled')) {
$hashids = new Hashids('', 10); //decoded output is _always_ an array. $hashids = new Hashids(config('ninja.hash_salt'), 10);
//parse URL hash and set DB //parse URL hash and set DB
$segments = explode('-', $request->route('confirmation_code')); $segments = explode('-', $request->route('confirmation_code'));
if(!is_array($segments))
return response()->json(['message' => 'Invalid confirmation code'], 403);
$hashed_db = $hashids->decode($segments[0]); $hashed_db = $hashids->decode($segments[0]);
if(!is_array($hashed_db))
return response()->json(['message' => 'Invalid confirmation code'], 403);
MultiDB::setDB(MultiDB::DB_PREFIX.str_pad($hashed_db[0], 2, '0', STR_PAD_LEFT)); MultiDB::setDB(MultiDB::DB_PREFIX.str_pad($hashed_db[0], 2, '0', STR_PAD_LEFT));
} }
return $next($request); return $next($request);
} }
} }

View File

@ -46,7 +46,8 @@ class CreateAccountRequest extends Request
} }
protected function prepareForValidation() protected function prepareForValidation()
{nlog($this->all()); {
$input = $this->all(); $input = $this->all();
$input['user_agent'] = request()->server('HTTP_USER_AGENT'); $input['user_agent'] = request()->server('HTTP_USER_AGENT');

View File

@ -54,6 +54,7 @@ class StoreClientRequest extends Request
/* Ensure we have a client name, and that all emails are unique*/ /* Ensure we have a client name, and that all emails are unique*/
//$rules['name'] = 'required|min:1'; //$rules['name'] = 'required|min:1';
$rules['settings'] = new ValidClientGroupSettingsRule(); $rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array';
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email'; $rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
$rules['contacts.*.password'] = [ $rules['contacts.*.password'] = [
'nullable', 'nullable',
@ -144,7 +145,10 @@ class StoreClientRequest extends Request
return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code; return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code;
})->first(); })->first();
return (string) $country->id; if($country)
return (string) $country->id;
return "";
} }
private function getCurrencyCode($code) private function getCurrencyCode($code)

View File

@ -62,6 +62,7 @@ class UpdateClientRequest extends Request
$rules['number'] = Rule::unique('clients')->where('company_id', auth()->user()->company()->id)->ignore($this->client->id); $rules['number'] = Rule::unique('clients')->where('company_id', auth()->user()->company()->id)->ignore($this->client->id);
$rules['settings'] = new ValidClientGroupSettingsRule(); $rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array';
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email'; $rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
$rules['contacts.*.password'] = [ $rules['contacts.*.password'] = [
'nullable', 'nullable',

View File

@ -37,8 +37,9 @@ class StoreClientGatewayTokenRequest extends Request
public function rules() public function rules()
{ {
//ensure client is present
$rules = [ $rules = [
'client_id' => 'required', 'client_id' => 'required|exists:clients,id,company_id,'.auth()->user()->company()->id,
'company_gateway_id' => 'required', 'company_gateway_id' => 'required',
'gateway_type_id' => 'required|integer', 'gateway_type_id' => 'required|integer',
'meta' => 'required', 'meta' => 'required',

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\ClientPortal\Contact;
use Illuminate\Foundation\Http\FormRequest;
class ContactPasswordResetRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required',
];
}
}

View File

@ -7,7 +7,7 @@
* *
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://www.elastic.co/licensing/elastic-license
*/ */
namespace App\Http\Requests\ClientPortal\Credits; namespace App\Http\Requests\ClientPortal\Credits;

Some files were not shown because too many files have changed in this diff Show More