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
MULTI_DB_ENABLED=false
# database
DB_CONNECTION=db-ninja-01
DB_CONNECTION=mysql
DB_DATABASE1=ninja
DB_USERNAME1=root
DB_PASSWORD1=ninja

View File

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

View File

@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
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']
env:
@ -49,6 +49,10 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
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
run: |
sudo /etc/init.d/mysql start

View File

@ -49,37 +49,16 @@ jobs:
sudo rm -rf bootstrap/cache/*
sudo rm -rf node_modules
sudo rm -rf .git
# - name: Prune Git History
# run: |
# 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
- name: Build project
run: |
zip -r ./invoiceninja.zip .* -x "../*"
- name: Get tag name
id: get_tag_name
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}-release
- name: Create Release
id: create_release
uses: actions/create-release@v1
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.get_tag_name.outputs.VERSION }}
release_name: Release ${{ steps.get_tag_name.outputs.VERSION }}
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
files: |
invoiceninja.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-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
Currently the client portal and API are of alpha quality, to get started:
```bash
git clone https://github.com/invoiceninja/invoiceninja.git
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)
Installation, Configuration and Troubleshooting documentation can be found [HERE] (https://invoiceninja.github.io)
## Credits
* [Hillel Coren](https://hillelcoren.com/)
* [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)
* [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.
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.
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)
## License
Invoice Ninja is released under the Attribution Assurance License.
Invoice Ninja is released under the Elastic License.
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\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyLedger;
use App\Models\Contact;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\Paymentable;
use App\Utils\Ninja;
use DB;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Mail;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Support\Str;
/*
@ -67,7 +69,7 @@ class CheckData extends Command
/**
* @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
@ -78,8 +80,14 @@ class CheckData extends Command
protected $isValid = true;
protected $wrong_paid_to_dates = 0;
protected $wrong_balances = 0;
public function handle()
{
$time_start = microtime(true);
$database_connection = $this->option('database') ? $this->option('database') : 'Connected to Default DB';
$fix_status = $this->option('fix') ? "Fixing Issues" : "Just checking issues ";
@ -92,6 +100,7 @@ class CheckData extends Command
$this->checkInvoiceBalances();
$this->checkInvoicePayments();
$this->checkPaidToDates();
// $this->checkPaidToCompanyDates();
$this->checkClientBalances();
$this->checkContacts();
$this->checkCompanyData();
@ -103,6 +112,8 @@ class CheckData extends Command
}
$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');
if ($errorEmail) {
@ -223,38 +234,38 @@ class CheckData extends Command
}
}
// check for more than one primary contact
$clients = DB::table('clients')
->leftJoin('client_contacts', function ($join) {
$join->on('client_contacts.client_id', '=', 'clients.id')
->where('client_contacts.is_primary', '=', true)
->whereNull('client_contacts.deleted_at');
})
->groupBy('clients.id')
->havingRaw('count(client_contacts.id) != 1');
// // check for more than one primary contact
// $clients = DB::table('clients')
// ->leftJoin('client_contacts', function ($join) {
// $join->on('client_contacts.client_id', '=', 'clients.id')
// ->where('client_contacts.is_primary', '=', true)
// ->whereNull('client_contacts.deleted_at');
// })
// ->groupBy('clients.id')
// ->havingRaw('count(client_contacts.id) != 1');
if ($this->option('client_id')) {
$clients->where('clients.id', '=', $this->option('client_id'));
}
// if ($this->option('client_id')) {
// $clients->where('clients.id', '=', $this->option('client_id'));
// }
$clients = $clients->get(['clients.id', 'clients.user_id', 'clients.company_id']);
$this->logMessage($clients->count().' clients without a single primary contact');
// $clients = $clients->get(['clients.id', 'clients.user_id', 'clients.company_id']);
// // $this->logMessage($clients->count().' clients without a single primary contact');
if ($this->option('fix') == 'true') {
foreach ($clients as $client) {
$this->logMessage("Fixing missing primary contacts #{$client->id}");
// // if ($this->option('fix') == 'true') {
// // foreach ($clients as $client) {
// // $this->logMessage("Fixing missing primary contacts #{$client->id}");
$new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
$new_contact->client_id = $client->id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->save();
}
}
// // $new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
// // $new_contact->client_id = $client->id;
// // $new_contact->contact_key = Str::random(40);
// // $new_contact->is_primary = true;
// // $new_contact->save();
// // }
// // }
if ($clients->count() > 0) {
$this->isValid = false;
}
// if ($clients->count() > 0) {
// $this->isValid = false;
// }
}
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()
{
$wrong_paid_to_dates = 0;
$this->wrong_paid_to_dates = 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;
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_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');
$total_invoice_payments += $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_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
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) {
$total_invoice_payments += $credit_total_applied;
}
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->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()
{
$wrong_balances = 0;
$wrong_paid_to_dates = 0;
$this->wrong_balances = 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 ($client) {
$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;
$client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($wrong_balances, $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_credit = $invoice->credits()->get()->sum('amount');
$total_paid = $total_amount - $total_refund;
$calculated_paid_amount = $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;
}
@ -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()
{
$wrong_balances = 0;
$wrong_paid_to_dates = 0;
$this->wrong_balances = 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 = 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');
@ -387,14 +502,15 @@ class CheckData extends Command
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
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->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 =
@ -406,27 +522,38 @@ class CheckData extends Command
private function checkInvoiceBalances()
{
$wrong_balances = 0;
$wrong_paid_to_dates = 0;
$this->wrong_balances = 0;
$this->wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->cursor() as $client) {
$invoice_balance = $client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get()->sum('balance');
$credit_balance = $client->credits()->where('is_deleted', false)->get()->sum('balance');
// if($client->balance != $invoice_balance)
// $invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount
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)->sum('balance');
$credit_balance = $client->credits()->where('is_deleted', false)->sum('balance');
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
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->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()
@ -482,6 +609,7 @@ class CheckData extends Command
'client',
'client_contact',
'payment',
'recurring_invoice',
],
'invoices' => [
'client',

View File

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

View File

@ -14,9 +14,11 @@ namespace App\Console\Commands;
use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits;
use App\Events\Invoice\InvoiceWasCreated;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
use App\Factory\GroupSettingFactory;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Factory\SubscriptionFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyTaskStatuses;
@ -48,6 +50,7 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use stdClass;
class CreateSingleAccount extends Command
{
@ -235,6 +238,9 @@ class CreateSingleAccount extends Command
$this->info('creating credit for client #' . $client->id);
$this->createCredit($client);
$this->info('creating recurring invoice for client # ' . $client->id);
$this->createRecurringInvoice($client);
}
$this->createGateways($company, $user);
@ -435,6 +441,10 @@ class CreateSingleAccount extends Command
$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->service()->createInvitations()->markSent();
@ -619,7 +629,7 @@ class CreateSingleAccount extends Command
$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;
@ -642,7 +652,7 @@ class CreateSingleAccount extends Command
$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;
@ -663,7 +673,7 @@ class CreateSingleAccount extends Command
$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;
@ -684,11 +694,141 @@ class CreateSingleAccount extends Command
$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;
$cg->fees_and_limits = $fees_and_limits;
$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 SchedulerCheck)->everyFiveMinutes();
$schedule->job(new SchedulerCheck)->daily()->withoutOverlapping();
/* Run hosted specific jobs */
if (Ninja::isHosted()) {
$schedule->job(new AdjustEmailQuota)->daily()->withoutOverlapping();
$schedule->job(new AdjustEmailQuota)->dailyAt('23:00')->withoutOverlapping();
$schedule->job(new SendFailedEmails)->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 $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 = true; //@implemented
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 $primary_font = 'Roboto';
public $secondary_font = 'Roboto';
public $primary_color = '#142cb5';
public $primary_color = '#298AAB';
public $secondary_color = '#7081e0';
public $hide_paid_to_date = false; //@TODO where?
@ -268,6 +269,7 @@ class CompanySettings extends BaseSettings
public $hide_empty_columns_on_pdf = false;
public static $casts = [
'show_all_tasks_client_portal' => 'string',
'entity_send_time' => 'int',
'shared_invoice_credit_counter' => 'bool',
'reply_to_name' => 'string',
@ -396,7 +398,6 @@ class CompanySettings extends BaseSettings
'email_template_reminder2' => 'string',
'email_template_reminder3' => 'string',
'email_template_reminder_endless' => 'string',
'enable_client_portal_password' => 'bool',
'inclusive_taxes' => 'bool',
'invoice_number_pattern' => 'string',
'invoice_number_counter' => 'integer',
@ -662,6 +663,7 @@ class CompanySettings extends BaseSettings
'$task.line_total',
],
'total_columns' => [
'$net_subtotal',
'$subtotal',
'$discount',
'$custom_surcharge1',

View File

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

View File

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

View File

@ -14,6 +14,7 @@ namespace App\Exceptions;
use App\Exceptions\FilePermissionsFailure;
use App\Exceptions\InternalPDFFailure;
use App\Exceptions\PhantomPDFFailure;
use App\Exceptions\StripeConnectFailure;
use App\Utils\Ninja;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
@ -77,21 +78,27 @@ class Handler extends ExceptionHandler
return;
}
if(Ninja::isHosted()){
if(Ninja::isHosted() && !($exception instanceof ValidationException)){
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;
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;
}
else
$key = 'Anonymous';
$scope->setUser([
'id' => 'Hosted_User',
'id' => $key,
'email' => 'hosted@invoiceninja.com',
'name' => $key,
'name' => $name,
]);
});
@ -120,7 +127,6 @@ class Handler extends ExceptionHandler
}
}
// if(config('ninja.expanded_logging'))
parent::report($exception);
}
@ -182,7 +188,7 @@ class Handler extends ExceptionHandler
} elseif ($exception instanceof NotFoundHttpException && $request->expectsJson()) {
return response()->json(['message'=>'Route does not exist'], 404);
} 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()) {
nlog($exception->validator->getMessageBag());
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()) {
return response()->json(['message' => $exception->getMessage()], 400);
} 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);

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['id']);
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) {
$invoice->{$key} = $value;

View File

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

View File

@ -29,7 +29,7 @@ class RecurringInvoiceToInvoiceFactory
$invoice->public_notes = $recurring_invoice->public_notes;
$invoice->private_notes = $recurring_invoice->private_notes;
//$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->line_items = $recurring_invoice->line_items;
$invoice->tax_name1 = $recurring_invoice->tax_name1;
@ -38,6 +38,8 @@ class RecurringInvoiceToInvoiceFactory
$invoice->tax_rate2 = $recurring_invoice->tax_rate2;
$invoice->tax_name3 = $recurring_invoice->tax_name3;
$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_value2 = $recurring_invoice->custom_value2;
$invoice->custom_value3 = $recurring_invoice->custom_value3;

View File

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

View File

@ -76,6 +76,11 @@ class ClientFilters extends QueryFilters
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.
*
@ -92,9 +97,11 @@ class ClientFilters extends QueryFilters
return $this->builder->where(function ($query) use ($filter) {
$query->where('clients.name', 'like', '%'.$filter.'%')
->orWhere('clients.id_number', 'like', '%'.$filter.'%')
->orWhere('client_contacts.first_name', 'like', '%'.$filter.'%')
->orWhere('client_contacts.last_name', 'like', '%'.$filter.'%')
->orWhere('client_contacts.email', 'like', '%'.$filter.'%')
->orWhereHas('contacts', function ($query) use($filter){
$query->where('first_name', 'like', '%'.$filter.'%');
$query->orWhere('last_name', 'like', '%'.$filter.'%');
$query->orWhere('email', 'like', '%'.$filter.'%');
})
->orWhere('clients.custom_value1', 'like', '%'.$filter.'%')
->orWhere('clients.custom_value2', '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) {
$query->where('expenses.name', 'like', '%'.$filter.'%')
->orWhere('expenses.id_number', 'like', '%'.$filter.'%')
->orWhere('expense_contacts.first_name', 'like', '%'.$filter.'%')
->orWhere('expense_contacts.last_name', 'like', '%'.$filter.'%')
->orWhere('expense_contacts.email', 'like', '%'.$filter.'%')
->orWhereHas('contacts', function ($query) use($filter){
$query->where('expense_contacts.first_name', '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_value2', 'like', '%'.$filter.'%')
->orWhere('expenses.custom_value3', 'like', '%'.$filter.'%')

View File

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

View File

@ -38,9 +38,11 @@ class VendorFilters extends QueryFilters
return $this->builder->where(function ($query) use ($filter) {
$query->where('vendors.name', 'like', '%'.$filter.'%')
->orWhere('vendors.id_number', 'like', '%'.$filter.'%')
->orWhere('vendor_contacts.first_name', 'like', '%'.$filter.'%')
->orWhere('vendor_contacts.last_name', 'like', '%'.$filter.'%')
->orWhere('vendor_contacts.email', 'like', '%'.$filter.'%')
->orWhereHas('contacts', function ($query) use($filter){
$query->where('vendor_contacts.first_name', '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_value2', 'like', '%'.$filter.'%')
->orWhere('vendors.custom_value3', 'like', '%'.$filter.'%')

View File

@ -1,5 +1,4 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -10,6 +9,8 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
use App\Utils\Ninja;
/**
* Simple helper function that will log into "invoiceninja.log" file
* only when extended logging is enabled.
@ -32,6 +33,15 @@ function nlog($output, $context = []): void
$trace = debug_backtrace();
//nlog( debug_backtrace()[1]['function']);
// \Illuminate\Support\Facades\Log::channel('invoiceninja')->info(print_r($trace[1]['class'],1), []);
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()
{ //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->item->cost * $this->item->quantity);
return $this;
@ -102,7 +103,15 @@ class InvoiceItemSum
if ($this->invoice->is_amount_discount) {
$this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision));
} 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;
@ -114,9 +123,6 @@ class InvoiceItemSum
{
$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));
$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 = new Mail;
nlog($message->getBcc());
/*We should nest the token in the message and then discard it as needed*/
$token = $message->getHeaders()->get('GmailToken')->getValue();
@ -62,13 +60,13 @@ class GmailTransport extends Transport
$this->gmail->message($message->getBody());
$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)
{
nlog("trying to attach");
if($child->getContentType() != 'text/plain')
{

View File

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

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Contact\ContactPasswordResetRequest;
use App\Libraries\MultiDB;
use App\Models\Account;
use Illuminate\Contracts\View\Factory;
@ -73,10 +74,8 @@ class ContactForgotPasswordController extends Controller
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'));
$this->validateEmail($request);

View File

@ -35,9 +35,16 @@ class ContactLoginController extends Controller
public function showLoginForm(Request $request)
{
if ($request->subdomain) {
$company = Company::where('subdomain', $request->subdomain)->first();
} elseif (Ninja::isSelfHost()) {
//if we are on the root domain invoicing.co do not show any company logos
if(Ninja::isHosted() && count(explode('.', request()->getHost())) == 2){
$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;
} else {
$company = null;

View File

@ -1,4 +1,13 @@
<?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;

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()
{
return Auth::guard('contact');

View File

@ -16,6 +16,7 @@ use App\DataMapper\Analytics\LoginSuccess;
use App\Events\User\UserLoggedIn;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\Controller;
use App\Http\Requests\Login\LoginRequest;
use App\Jobs\Account\CreateAccount;
use App\Jobs\Company\CreateCompanyToken;
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'];
@ -179,8 +180,6 @@ class LoginController extends BaseController
$user = $this->guard()->user();
event(new UserLoggedIn($user, $user->account->default_company, Ninja::eventVars($user->id)));
//2FA
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'));
}
/* 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);
$this->setLoginCache($user);
@ -226,6 +233,8 @@ class LoginController extends BaseController
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);
event(new UserLoggedIn($user, $user->account->default_company, Ninja::eventVars($user->id)));
return $this->timeConstrainedResponse($cu);
@ -300,7 +309,6 @@ class LoginController extends BaseController
$cu = CompanyUser::query()
->where('user_id', $company_token->user_id);
$cu->first()->account->companies->each(function ($company) use($cu, $request){
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')
$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->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_user, true);
$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($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);
$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->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_login_user, true);
$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()->save();
auth()->user()->setCompany(auth()->user()->account->default_company);
$this->setLoginCache(auth()->user());
$cu = CompanyUser::whereUserId(auth()->user()->id);

View File

@ -112,6 +112,7 @@ class BaseController extends Controller
'company.groups',
'company.payment_terms',
'company.designs.company',
'company.expense_categories',
];
public function __construct()
@ -202,6 +203,10 @@ class BaseController extends Controller
$transformer = new $this->entity_transformer($this->serializer);
$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);
$query->with(
@ -379,8 +384,6 @@ class BaseController extends Controller
'company.designs'=> function ($query) use ($created_at, $user) {
$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) {
$query->where('created_at', '>=', $created_at);
@ -388,22 +391,14 @@ class BaseController extends Controller
'company.groups' => function ($query) use ($created_at, $user) {
$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) {
$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) {
$query->where('created_at', '>=', $created_at);
if(!$user->isAdmin())
$query->where('tax_rates.user_id', $user->id);
},
'company.activities'=> function ($query) use($user) {
@ -519,8 +514,6 @@ class BaseController extends Controller
'company.payment_terms'=> function ($query) use ($created_at, $user) {
$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) {
@ -561,9 +554,6 @@ class BaseController extends Controller
'company.tax_rates' => function ($query) use ($created_at, $user) {
$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) {
$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) {
$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) {
$query->where('created_at', '>=', $created_at);
@ -748,11 +735,14 @@ class BaseController extends Controller
$data = [];
if (Ninja::isSelfHost()) {
$data['report_errors'] = $account->report_errors;
} else {
$data['report_errors'] = true;
}
//pass report errors bool to front end
$data['report_errors'] = Ninja::isSelfHost() ? $account->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();
@ -762,6 +752,29 @@ class BaseController extends Controller
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)
{

View File

@ -510,7 +510,7 @@ class ClientController extends BaseController
$action = request()->input('action');
$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) {
if (auth()->user()->can('edit', $client)) {

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller;
use App\Models\RecurringInvoice;
use Auth;
class ContactHashLoginController extends Controller
@ -24,6 +25,17 @@ class ContactHashLoginController extends Controller
*/
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');
}

View File

@ -24,6 +24,7 @@ use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
class InvoiceController extends Controller
{
@ -89,6 +90,7 @@ class InvoiceController extends Controller
{
$invoices = Invoice::whereIn('id', $ids)
->whereClientId(auth()->user()->client->id)
->withTrashed()
->get();
//filter invoices which are payable
@ -167,9 +169,12 @@ class InvoiceController extends Controller
if ($invoices->count() == 1) {
$invoice = $invoices->first();
$invitation = $invoice->invitations->first();
$file = $invoice->pdf_file_path($invitation);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);;
//$file = $invoice->pdf_file_path($invitation);
$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
@ -180,7 +185,10 @@ class InvoiceController extends Controller
$zip = new ZipStream(date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip', $options);
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

View File

@ -95,7 +95,7 @@ class PaymentController extends Controller
*/
$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){
$invoice->service()->removeUnpaidGatewayFees()->save();
@ -243,6 +243,10 @@ class PaymentController extends Controller
->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))];
if ($request->query('hash')) {
@ -257,11 +261,19 @@ class PaymentController extends Controller
$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 = [
'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals,
'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals)),
'amount_with_fee' => $amount_with_fee,
];
$data = [
@ -273,7 +285,7 @@ class PaymentController extends Controller
'amount_with_fee' => $invoice_totals + $fee_totals,
];
if ($is_credit_payment) {
if ($is_credit_payment || $totals <= 0) {
return $this->processCreditPayment($request, $data);
}
@ -320,6 +332,7 @@ class PaymentController extends Controller
*/
public function credit_response(Request $request)
{
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->first();
/* Hydrate the $payment */

View File

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

View File

@ -7,7 +7,7 @@
*
* @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;
@ -24,8 +24,10 @@ use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
class QuoteController extends Controller
{
@ -46,7 +48,7 @@ class QuoteController extends Controller
*
* @param ShowQuoteRequest $request
* @param Quote $quote
* @return Factory|View|\Symfony\Component\HttpFoundation\BinaryFileResponse
* @return Factory|View|BinaryFileResponse
*/
public function show(ShowQuoteRequest $request, Quote $quote)
{
@ -83,13 +85,18 @@ class QuoteController extends Controller
->get();
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) {
$file = $quotes->first()->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
$file = $quotes->first()->service()->getQuotePdf();
// 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
@ -100,7 +107,9 @@ class QuoteController extends Controller
$zip = new ZipStream(date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip', $options);
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
@ -110,11 +119,15 @@ class QuoteController extends Controller
protected function approve(array $ids, $process = false)
{
$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();
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) {

View File

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

View File

@ -25,7 +25,10 @@ use App\Jobs\Company\CreateCompany;
use App\Jobs\Company\CreateCompanyPaymentTerms;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Company\CreateCompanyToken;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Ninja\RefundCancelledAccount;
use App\Mail\Company\CompanyDeleted;
use App\Models\Account;
use App\Models\Company;
use App\Models\CompanyUser;
@ -474,10 +477,16 @@ class CompanyController extends BaseController
*/
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();
$account = $company->account;
$account_key = $account->key;
if ($company_count == 1) {
$company->company_users->each(function ($company_user) {
$company_user->user->forceDelete();
$company_user->forceDelete();
@ -485,9 +494,13 @@ class CompanyController extends BaseController
$account->delete();
if(Ninja::isHosted())
\Modules\Admin\Jobs\Account\NinjaDeletedAccount::dispatch($account_key);
LightLogs::create(new AccountDeleted())
->increment()
->batch();
} else {
$company_id = $company->id;
@ -495,6 +508,15 @@ class CompanyController extends BaseController
$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();
//If we are deleting the default companies, we'll need to make a new company the default.
@ -571,4 +593,9 @@ class CompanyController extends BaseController
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)
{
$company_gateway->driver(new Client)
->disconnect();
$company_gateway->delete();
return $this->itemResponse($company_gateway->fresh());
}
/**

View File

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

View File

@ -104,8 +104,13 @@ class ConnectedAccountController extends BaseController
$refresh_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 = [
'email' => $google->harvestEmail($user),
'email' => $email,
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id' => 'google',
'email_verified_at' =>now()

View File

@ -37,6 +37,7 @@ use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class CreditController.
@ -536,8 +537,14 @@ class CreditController extends BaseController
}
break;
case 'download':
$file = $credit->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// $file = $credit->pdf_file_path();
$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;
case 'archive':
$this->credit_repository->archive($credit);
@ -585,9 +592,12 @@ class CreditController extends BaseController
// $contact = $invitation->contact;
$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();
EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data)
->delay(now()->addSeconds(30));
EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data);
// ->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;
use App\Exceptions\NonExistingMigrationFile;
use App\Http\Requests\Import\ImportJsonRequest;
use App\Jobs\Company\CompanyExport;
use App\Jobs\Company\CompanyImport;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use ZipArchive;
use Illuminate\Support\Facades\Storage;
class ImportJsonController extends BaseController
{
@ -60,40 +63,19 @@ class ImportJsonController extends BaseController
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());
$hash = Str::random(32);
nlog($hash);
Cache::put( $hash, base64_encode( $contents ), 3600 );
CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $hash, $request->except('files'))->delay(now()->addMinutes(1));
if(Ninja::isHosted())
CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $file_location, $request->except('files'))->onQueue('migration');
else
CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $file_location, $request->except('files'));
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;
case 'download':
$file = $invoice->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
$file = $invoice->service()->getInvoicePdf();
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
break;
case 'restore':
@ -722,10 +726,11 @@ class InvoiceController extends BaseController
}
//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) {
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) {
@ -790,13 +795,18 @@ class InvoiceController extends BaseController
public function downloadPdf($invitation_key)
{
$invitation = $this->invoice_repo->getInvitationByKey($invitation_key);
if(!$invitation)
return response()->json(["message" => "no record found"], 400);
$contact = $invitation->contact;
$invoice = $invitation->invoice;
$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);
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\Response;
use stdClass;
use Carbon\Carbon;
class LicenseController extends BaseController
{
@ -152,7 +153,7 @@ class LicenseController extends BaseController
{
$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_expires = null;
$account->save();

View File

@ -82,6 +82,9 @@ class MigrationController extends BaseController
*/
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;
$company_id = $company->id;
@ -102,6 +105,9 @@ class MigrationController extends BaseController
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;
$company_id = $company->id;
@ -387,7 +393,6 @@ class MigrationController extends BaseController
else
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));
if($request->has('email_receipt') && $request->input('email_receipt') == 'true' && !$payment->client->getSetting('client_manual_payment_notification'))
$payment->service()->sendEmail();
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;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Libraries\MultiDB;
use Auth;
class PaymentWebhookController extends Controller
{
public function __invoke(PaymentWebhookRequest $request, string $company_key, string $company_gateway_id)
public function __invoke(PaymentWebhookRequest $request)
{
// MultiDB::findAndSetDbByCompanyKey($company_key);
$payment = $request->getPayment();
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);
return $request
->getCompanyGateway()
->driver()
->processWebhookRequest($request);
}
}

View File

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

View File

@ -11,11 +11,28 @@
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\Libraries\MultiDB;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\Invoice;
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\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf;
@ -28,6 +45,7 @@ use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Response;
use Turbo124\Beacon\Facades\LightLogs;
class PreviewController extends BaseController
{
@ -118,6 +136,7 @@ class PreviewController extends BaseController
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $entity_obj->client->company->markdown_enabled,
];
$design = new Design(request()->design['name']);
@ -149,13 +168,150 @@ class PreviewController extends BaseController
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()
{
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations(auth()->user()->company()->settings));
DB::beginTransaction();
DB::connection(auth()->user()->company()->db)->beginTransaction();
$client = Client::factory()->create([
'user_id' => auth()->user()->id,
@ -208,6 +364,7 @@ class PreviewController extends BaseController
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $invoice->client->company->markdown_enabled,
];
$maker = new PdfMaker($state);
@ -230,7 +387,7 @@ class PreviewController extends BaseController
$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->header('Content-Type', 'application/pdf');

View File

@ -470,7 +470,7 @@ class ProductController extends BaseController
$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) {
if (auth()->user()->can('edit', $product)) {

View File

@ -39,6 +39,7 @@ use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class QuoteController.
@ -676,8 +677,14 @@ class QuoteController extends BaseController
break;
case 'download':
$file = $quote->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
//$file = $quote->pdf_file_path();
$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;
case 'restore':
@ -728,9 +735,13 @@ class QuoteController extends BaseController
$contact = $invitation->contact;
$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 Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class RecurringInvoiceController.
@ -500,9 +501,12 @@ class RecurringInvoiceController extends BaseController
$contact = $invitation->contact;
$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\StoreSetupRequest;
use App\Jobs\Account\CreateAccount;
use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Utils\CurlUtils;
@ -69,8 +70,6 @@ class SetupController extends Controller
}
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. */
}
@ -109,11 +108,11 @@ class SetupController extends Controller
'REQUIRE_HTTPS' => $request->input('https') ? 'true' : 'false',
'APP_DEBUG' => 'false',
'DB_HOST1' => $request->input('db_host'),
'DB_PORT1' => $request->input('db_port'),
'DB_DATABASE1' => $request->input('db_database'),
'DB_USERNAME1' => $request->input('db_username'),
'DB_PASSWORD1' => $request->input('db_password'),
'DB_HOST' => $request->input('db_host'),
'DB_PORT' => $request->input('db_port'),
'DB_DATABASE' => $request->input('db_database'),
'DB_USERNAME' => $request->input('db_username'),
'DB_PASSWORD' => $request->input('db_password'),
'MAIL_MAILER' => $mail_driver,
'MAIL_PORT' => $request->input('mail_port'),
@ -125,6 +124,7 @@ class SetupController extends Controller
'MAIL_PASSWORD' => $request->input('mail_password'),
'NINJA_ENVIRONMENT' => 'selfhost',
'DB_CONNECTION' => 'mysql',
];
if (config('ninja.db.multi_db_enabled')) {
@ -133,11 +133,11 @@ class SetupController extends Controller
if (config('ninja.preconfigured_install')) {
// Database connection was already configured. Don't let the user override it.
unset($env_values['DB_HOST1']);
unset($env_values['DB_PORT1']);
unset($env_values['DB_DATABASE1']);
unset($env_values['DB_USERNAME1']);
unset($env_values['DB_PASSWORD1']);
unset($env_values['DB_HOST']);
unset($env_values['DB_PORT']);
unset($env_values['DB_DATABASE']);
unset($env_values['DB_USERNAME']);
unset($env_values['DB_PASSWORD']);
}
try {
@ -240,6 +240,11 @@ class SetupController extends Controller
$pdf->setChromiumPath(config('ninja.snappdf_chromium_path'));
}
if (config('ninja.snappdf_chromium_arguments')) {
$pdf->clearChromiumArguments();
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
}
$pdf = $pdf
->setHtml('GENERATING PDFs WORKS! Thank you for using Invoice Ninja!')
->generate();
@ -275,10 +280,7 @@ class SetupController extends Controller
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')) )
return redirect('/');
@ -307,6 +309,8 @@ class SetupController extends Controller
$this->buildCache(true);
SchedulerCheck::dispatchNow();
return redirect('/');
}

View File

@ -1,5 +1,4 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -52,7 +51,7 @@ class StripeConnectController extends BaseController
$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');
}
@ -61,12 +60,6 @@ class StripeConnectController extends BaseController
$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}";
// 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);
}
@ -88,17 +81,26 @@ class StripeConnectController extends BaseController
}
// nlog($response);
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = Company::where('company_key', $request->getTokenContent()['company_key'])->first();
$company_gateway = CompanyGateway::query()
->where('gateway_key', 'd14dd26a47cecc30fdd65700bfb67b34')
->where('company_id', $company->id)
->first();
if(!$company_gateway)
{
$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 = [
'account_id' => $response->stripe_user_id,
@ -111,18 +113,6 @@ class StripeConnectController extends BaseController
"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->save();

View File

@ -14,10 +14,14 @@ namespace App\Http\Controllers;
use App\Jobs\Util\ImportStripeCustomers;
use App\Jobs\Util\StripeUpdatePaymentMethods;
use App\Models\Client;
use App\Models\CompanyGateway;
class StripeController extends BaseController
{
private $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23'];
public function update()
{
if(auth()->user()->isAdmin())
@ -29,13 +33,15 @@ class StripeController extends BaseController
}
return response()->json(['message' => 'Unauthorized'], 403);
}
public function import()
{
// return response()->json(['message' => 'Processing'], 200);
if(auth()->user()->isAdmin())
{
@ -45,8 +51,25 @@ class StripeController extends BaseController
}
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',
'client_domain',
'custom_domain',
'preview',
'invoiceninja',
'cname',
];
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)));
nlog($subscription->id);
return $this->itemResponse($subscription);
}

View File

@ -76,7 +76,7 @@ class SendingController extends Controller
}
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([
'success' => true,

View File

@ -11,11 +11,17 @@
namespace App\Http\Controllers;
use PragmaRX\Google2FA\Google2FA;
use App\Models\User;
use App\Transformers\UserTransformer;
use Crypt;
use PragmaRX\Google2FA\Google2FA;
class TwoFactorController extends BaseController
{
protected $entity_type = User::class;
protected $entity_transformer = UserTransformer::class;
public function setupTwoFactor()
{
$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\SetDb;
use App\Http\Middleware\SetDbByCompanyKey;
use App\Http\Middleware\SetDocumentDb;
use App\Http\Middleware\SetDomainNameDb;
use App\Http\Middleware\SetEmailDb;
use App\Http\Middleware\SetInviteDb;
@ -158,6 +159,7 @@ class Kernel extends HttpKernel
'contact_key_login' => ContactKeyLogin::class,
'check_client_existence' => CheckClientExistence::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());
$data = [
'name' => 'Client Name',
'name' => '',
'contacts' => [
['email' => $this->email],
],

View File

@ -34,9 +34,15 @@ class CreditsTable extends Component
public function render()
{
$query = Credit::query()
->where('client_id', auth('contact')->user()->client->id)
->where('company_id', $this->company->id)
->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')
->paginate($this->per_page);

View File

@ -44,6 +44,7 @@ class InvoicesTable extends Component
$query = Invoice::query()
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->where('company_id', $this->company->id)
->where('is_deleted', false);
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()
->with('gateway_type')
->where('company_id', $this->company->id)
->where('client_id', $this->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page);

View File

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

View File

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

View File

@ -45,6 +45,7 @@ class QuotesTable extends Component
}
$query = $query
->where('company_id', $this->company->id)
->where('client_id', auth('contact')->user()->client->id)
->where('status_id', '<>', Quote::STATUS_DRAFT)
->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;
use App\Libraries\MultiDB;
use App\Models\RecurringInvoice;
use App\Utils\Traits\WithSorting;
use Livewire\Component;
@ -23,8 +24,12 @@ class RecurringInvoicesTable extends Component
public $per_page = 10;
public $company;
public function mount()
{
MultiDB::setDb($this->company->db);
$this->sort_asc = false;
$this->sort_field = 'date';
@ -36,6 +41,7 @@ class RecurringInvoicesTable extends Component
$query = $query
->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])
->orderBy('status_id', 'asc')
->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()
{
count($this->fields) > 0

View File

@ -90,6 +90,16 @@ class SubscriptionPlanSwitch extends Component
$this->state['show_loading_bar'] = true;
$payment_required = $this->target->service()->changePlanPaymentCheck([
'recurring_invoice' => $this->recurring_invoice,
'subscription' => $this->subscription,
'target' => $this->target,
'hash' => $this->hash,
]);
if($payment_required)
{
$this->state['invoice'] = $this->target->service()->createChangePlanInvoice([
'recurring_invoice' => $this->recurring_invoice,
'subscription' => $this->subscription,
@ -109,6 +119,12 @@ class SubscriptionPlanSwitch extends Component
$this->state['payment_initialised'] = true;
}
else
$this->handlePaymentNotRequired();
$this->emit('beforePaymentEventsCompleted');
}

View File

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

View File

@ -35,8 +35,18 @@ class TasksTable extends Component
public function render()
{
$query = Task::query()
->where('client_id', auth('contact')->user()->client->id)
->whereNotNull('invoice_id')
->where('company_id', $this->company->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')
->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)
{
$multiple_contacts = ClientContact::query()
->with('company','client')
->where('email', auth('contact')->user()->email)
->whereNotNull('email')
->where('email', '<>', '')

View File

@ -71,11 +71,16 @@ class ContactKeyLogin
}
} elseif ($request->segment(2) && $request->segment(2) == 'key_login' && $request->segment(3)) {
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();
}
auth()->guard('contact')->login($client_contact, true);
if ($request->query('next')) {
return redirect($request->query('next'));
}
return redirect()->to('client/dashboard');
}
} elseif ($request->has('client_hash') && config('ninja.db.multi_db_enabled')) {
@ -106,7 +111,6 @@ class ContactKeyLogin
}
}
return $next($request);
}
}

View File

@ -33,7 +33,8 @@ class ContactRegister
if($company)
{
abort_unless($company->client_can_register, 404);
if(! $company->client_can_register)
abort(400, 'Registration disabled');
$request->merge(['key' => $company->company_key]);
@ -49,7 +50,9 @@ class ContactRegister
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]);
@ -62,7 +65,10 @@ class ContactRegister
if ($request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$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);
}
@ -72,7 +78,8 @@ class ContactRegister
if (!$request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$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]);

View File

@ -44,6 +44,14 @@ class PasswordProtection
else
$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')) {
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
@ -58,9 +66,6 @@ class PasswordProtection
$google = new Google();
$user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD'));
nlog("user");
nlog($user);
if (is_array($user)) {
$query = [
@ -68,10 +73,8 @@ class PasswordProtection
'oauth_provider_id'=> 'google'
];
nlog($query);
//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");
@ -91,7 +94,7 @@ class PasswordProtection
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);

View File

@ -52,11 +52,18 @@ class QueryLogging
$timeEnd = microtime(true);
$time = $timeEnd - $timeStart;
//nlog($request->method().' - '.urldecode($request->url()).": $count queries - ".$time);
// if($count > 50)
// if($count > 150)
// nlog($queries);
LightLogs::create(new DbQuery($request->method(), urldecode($request->url()), $count, $time, request()->ip()))
$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();
}

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)
{
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
$segments = explode('-', $request->route('confirmation_code'));
if(!is_array($segments))
return response()->json(['message' => 'Invalid confirmation code'], 403);
$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));
}
return $next($request);
}
}

View File

@ -46,7 +46,8 @@ class CreateAccountRequest extends Request
}
protected function prepareForValidation()
{nlog($this->all());
{
$input = $this->all();
$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*/
//$rules['name'] = 'required|min:1';
$rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array';
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
$rules['contacts.*.password'] = [
'nullable',
@ -144,7 +145,10 @@ class StoreClientRequest extends Request
return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code;
})->first();
if($country)
return (string) $country->id;
return "";
}
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['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array';
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
$rules['contacts.*.password'] = [
'nullable',

View File

@ -37,8 +37,9 @@ class StoreClientGatewayTokenRequest extends Request
public function rules()
{
//ensure client is present
$rules = [
'client_id' => 'required',
'client_id' => 'required|exists:clients,id,company_id,'.auth()->user()->company()->id,
'company_gateway_id' => 'required',
'gateway_type_id' => 'required|integer',
'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)
*
* @license https://opensource.org/licenses/AAL
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\ClientPortal\Credits;

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