Merge branch 'v5-develop' into v5-develop

This commit is contained in:
Lars Kusch 2023-04-05 17:32:43 +02:00 committed by GitHub
commit 9b7845d4b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
456 changed files with 371369 additions and 353640 deletions

View File

@ -83,6 +83,66 @@ http://localhost:8000/client/login - For Client Portal
user: user@example.com
pass: password
```
## Developers Guide
### App Design
The API and client portal have been developed using [Laravel](https://laravel.com) if you wish to contribute to this project familiarity with Laravel is essential.
When inspecting functionality of the API, the best place to start would be in the routes/api.php file which describes all of the availabe API endpoints. The controller methods then describe all the entry points into each domain of the application, ie InvoiceController / QuoteController
The average API request follows this path into the application.
* Middleware processes the request initially inspecting the domain being requested + provides the authentication layer.
* The request then passes into a Form Request (Type hinted in the controller methods) which is used to provide authorization and also validation of the request. If successful, the request is then passed into the controller method where it is digested, here is an example:
```php
public function store(StoreInvoiceRequest $request)
{
$invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id));
$invoice = $invoice->service()
->fillDefaults()
->triggeredActions($request)
->adjustInventory()
->save();
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
return $this->itemResponse($invoice);
}
```
Here for example we are storing a new invoice, we pass the validated request along with a factory into the invoice repository where it is processed and saved.
The returned invoice then passes through its service class (app/Services/Invoice) where various actions are performed.
A event is then fired which notifies listeners in the application (app/Providers/EventServiceProvider) which perform non blocking sub tasks
Finally the invoice is transformed (app/Transformers/) and returned as a response via Fractal.
### Developer environment
Using the Quick Hosting Setup describe above you can quickly get started building out your development environment. Instead of using
```
composer i -o --no-dev
```
use
```
composer i -o
```
This provides the developer tools including phpunit which allows the test suite to be run.
If you are considering contributing back to the main repository, please add in any tests for new functionality / modifications. This will greatly increase the chances of your PR being accepted
Also, if you plan any additions for the main repository, you may want to discuss this with us first on Slack where we can assist with any technical information and provide advice.
## Credits
* [Hillel Coren](https://hillelcoren.com/)

View File

@ -1 +1 @@
5.5.88
5.5.102

View File

@ -190,14 +190,12 @@ class BackupUpdate extends Command
->where('filename', '!=', '')
->cursor()
->each(function ($backup) {
$backup_bin = Storage::disk('s3')->get($backup->filename);
if ($backup_bin) {
Storage::disk($this->option('disk'))->put($backup->filename, $backup_bin);
nlog("Backups - Moving {$backup->filename} to {$this->option('disk')}");
}
});
}

View File

@ -21,6 +21,7 @@ use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyLedger;
use App\Models\CompanyToken;
use App\Models\CompanyUser;
use App\Models\Contact;
use App\Models\Credit;
@ -160,16 +161,33 @@ class CheckData extends Command
private function checkCompanyTokens()
{
CompanyUser::doesnthave('token')->cursor()->each(function ($cu) {
if ($cu->user) {
$this->logMessage("Creating missing company token for user # {$cu->user->id} for company id # {$cu->company->id}");
(new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle();
} else {
$this->logMessage("Dangling User ID # {$cu->id}");
// CompanyUser::whereDoesntHave('token', function ($query){
// return $query->where('is_system', 1);
// })->cursor()->each(function ($cu){
// if ($cu->user) {
// $this->logMessage("Creating missing company token for user # {$cu->user->id} for company id # {$cu->company->id}");
// (new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle();
// } else {
// $this->logMessage("Dangling User ID # {$cu->id}");
// }
// });
CompanyUser::query()->cursor()->each(function ($cu) {
if (CompanyToken::where('user_id', $cu->user_id)->where('company_id', $cu->company_id)->where('is_system', 1)->doesntExist()) {
$this->logMessage("Creating missing company token for user # {$cu->user_id} for company id # {$cu->company_id}");
if ($cu->company && $cu->user) {
(new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle();
}
}
});
}
/**
* checkOauthSanity
*
* @return void
*/
private function checkOauthSanity()
{
User::where('oauth_provider_id', '1')->cursor()->each(function ($user) {

View File

@ -188,6 +188,7 @@ class DemoMode extends Command
$company_token->account_id = $account->id;
$company_token->name = 'test token';
$company_token->token = 'TOKEN';
$company_token->is_system = true;
$company_token->save();
$u2->companies()->attach($company->id, [

View File

@ -51,8 +51,8 @@ class ReactBuilder extends Command
$directoryIterator = new \RecursiveDirectoryIterator(public_path('react'), \RecursiveDirectoryIterator::SKIP_DOTS);
foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) {
if (str_contains($file->getFileName(), '.js') && !strpos($file->getFileName(), '.json')) {
if (str_contains($file->getFileName(), 'index.')) {
if ($file->getExtension() == 'js') {
if (str_contains($file->getFileName(), 'index-')) {
$includes .= '<script type="module" crossorigin src="/react/'.$file->getFileName().'"></script>'."\n";
} else {
$includes .= '<link rel="modulepreload" href="/react/'.$file->getFileName().'">'."\n";

View File

@ -15,6 +15,7 @@ use App\Jobs\Cron\AutoBillCron;
use App\Jobs\Cron\RecurringExpensesCron;
use App\Jobs\Cron\RecurringInvoicesCron;
use App\Jobs\Cron\SubscriptionCron;
use App\Jobs\Cron\UpdateCalculatedFields;
use App\Jobs\Invoice\InvoiceCheckLateWebhook;
use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\BankTransactionSync;
@ -27,7 +28,6 @@ use App\Jobs\Subscription\CleanStaleInvoiceOrder;
use App\Jobs\Util\DiskCleanup;
use App\Jobs\Util\ReminderJob;
use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\SendFailedEmails;
use App\Jobs\Util\UpdateExchangeRates;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
@ -48,14 +48,23 @@ class Kernel extends ConsoleKernel
/* Check for the latest version of Invoice Ninja */
$schedule->job(new VersionCheck)->daily();
/* Checks and cleans redundant files */
$schedule->job(new DiskCleanup)->dailyAt('02:10')->withoutOverlapping()->name('disk-cleanup-job')->onOneServer();
/* Returns the number of jobs in the queue */
$schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping()->name('queue-size-job')->onOneServer();
/* Send reminders */
$schedule->job(new ReminderJob)->hourly()->withoutOverlapping()->name('reminder-job')->onOneServer();
/* Returns the number of jobs in the queue */
$schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping()->name('queue-size-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
/* Checks for scheduled tasks */
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new CleanStaleInvoiceOrder)->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new UpdateCalculatedFields)->hourlyAt(40)->withoutOverlapping()->name('update-calculated-fields-job')->onOneServer();
/* Checks for large companies and marked them as is_large */
$schedule->job(new CompanySizeCheck)->dailyAt('23:20')->withoutOverlapping()->name('company-size-job')->onOneServer();
@ -66,33 +75,26 @@ class Kernel extends ConsoleKernel
/* Runs cleanup code for subscriptions */
$schedule->job(new SubscriptionCron)->dailyAt('00:01')->withoutOverlapping()->name('subscription-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new CleanStaleInvoiceOrder)->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer();
/* Fires notifications for expired Quotes */
$schedule->job(new QuoteCheckExpired)->dailyAt('05:10')->withoutOverlapping()->name('quote-expired-job')->onOneServer();
/* Fires webhooks for overdue Invoice */
$schedule->job(new InvoiceCheckLateWebhook)->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-job')->onOneServer();
/* Performs auto billing */
$schedule->job(new AutoBillCron)->dailyAt('06:20')->withoutOverlapping()->name('auto-bill-job')->onOneServer();
/* Checks the status of the scheduler */
$schedule->job(new SchedulerCheck)->dailyAt('01:10')->withoutOverlapping();
/* Checks for scheduled tasks */
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
/* Checks and cleans redundant files */
$schedule->job(new DiskCleanup)->dailyAt('02:10')->withoutOverlapping()->name('disk-cleanup-job')->onOneServer();
/* Performs system maintenance such as pruning the backup table */
$schedule->job(new SystemMaintenance)->sundays()->at('02:30')->withoutOverlapping()->name('system-maintenance-job')->onOneServer();
/* Fires notifications for expired Quotes */
$schedule->job(new QuoteCheckExpired)->dailyAt('05:10')->withoutOverlapping()->name('quote-expired-job')->onOneServer();
/* Performs auto billing */
$schedule->job(new AutoBillCron)->dailyAt('06:20')->withoutOverlapping()->name('auto-bill-job')->onOneServer();
/* Fires webhooks for overdue Invoice */
$schedule->job(new InvoiceCheckLateWebhook)->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-job')->onOneServer();
if (Ninja::isSelfHost()) {
$schedule->call(function () {
@ -107,9 +109,6 @@ class Kernel extends ConsoleKernel
/* Pulls in bank transactions from third party services */
$schedule->job(new BankTransactionSync)->dailyAt('04:10')->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
//not used @deprecate
// $schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->dailyAt('02:10')->withoutOverlapping()->name('check-data-db-1-job')->onOneServer();
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:20')->withoutOverlapping()->name('check-data-db-2-job')->onOneServer();

View File

@ -84,13 +84,13 @@ class ClientSettings extends BaseSettings
*/
public static function buildClientSettings($company_settings, $client_settings)
{
if (! $client_settings) {
return $company_settings;
}
if(is_array($client_settings))
if (is_array($client_settings)) {
$client_settings = (object)$client_settings;
}
foreach ($company_settings as $key => $value) {
/* pseudo code

View File

@ -449,6 +449,8 @@ class CompanySettings extends BaseSettings
public $mailgun_domain = '';
public $mailgun_endpoint = 'api.mailgun.net'; //api.eu.mailgun.net
public $auto_bill_standard_invoices = false;
public $email_alignment = 'center'; // center , left, right
@ -467,7 +469,17 @@ class CompanySettings extends BaseSettings
public $show_task_item_description = false;
public $client_initiated_payments = false;
public $client_initiated_payments_minimum = 0;
public $sync_invoice_quote_columns = true;
public static $casts = [
'mailgun_endpoint' => 'string',
'client_initiated_payments' => 'bool',
'client_initiated_payments_minimum' => 'float',
'sync_invoice_quote_columns' => 'bool',
'show_task_item_description' => 'bool',
'allow_billable_task_items' => 'bool',
'accept_client_input_quote_approval' => 'bool',
@ -493,7 +505,6 @@ class CompanySettings extends BaseSettings
'purchase_order_design_id' => 'string',
'purchase_order_footer' => 'string',
'purchase_order_number_pattern' => 'string',
'purchase_order_number_counter' => 'int',
'page_numbering_alignment' => 'string',
'page_numbering' => 'bool',
'auto_archive_invoice_cancelled' => 'bool',
@ -525,7 +536,6 @@ class CompanySettings extends BaseSettings
'reminder_send_time' => 'int',
'email_sending_method' => 'string',
'gmail_sending_user_id' => 'string',
'currency_id' => 'string',
'counter_number_applied' => 'string',
'quote_number_applied' => 'string',
'email_subject_custom1' => 'string',
@ -907,6 +917,15 @@ class CompanySettings extends BaseSettings
'$product.tax',
'$product.line_total',
],
'product_quote_columns' => [
'$product.item',
'$product.description',
'$product.unit_cost',
'$product.quantity',
'$product.discount',
'$product.tax',
'$product.line_total',
],
'task_columns' =>[
'$task.service',
'$task.description',

View File

@ -59,7 +59,11 @@ class InvoiceItem
public $type_id = '1'; //1 = product, 2 = service, 3 unpaid gateway fee, 4 paid gateway fee, 5 late fee, 6 expense
public $tax_id = '';
public static $casts = [
'tax_id' => 'string',
'type_id' => 'string',
'quantity' => 'float',
'cost' => 'float',

View File

@ -0,0 +1,36 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Schedule;
class EmailRecord
{
/**
* Defines the template name
*
* @var string
*/
public string $template = 'email_record';
/**
* Defines the template name
*
* @var string
*/
public string $entity = ''; // invoice, credit, quote, purchase_order
/**
* Defines the template name
*
* @var string
*/
public string $entity_id = '';
}

View File

@ -0,0 +1,156 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Tax;
use App\DataMapper\Tax\ZipTax\Response;
use App\Models\Client;
class BaseRule implements RuleInterface
{
/** EU TAXES */
public bool $consumer_tax_exempt = false;
public bool $business_tax_exempt = true;
public bool $eu_business_tax_exempt = true;
public bool $foreign_business_tax_exempt = true;
public bool $foreign_consumer_tax_exempt = true;
public array $eu_country_codes = [
'AT', // Austria
'BE', // Belgium
'BG', // Bulgaria
'CY', // Cyprus
'CZ', // Czech Republic
'DE', // Germany
'DK', // Denmark
'EE', // Estonia
'ES', // Spain
'FI', // Finland
'FR', // France
'GR', // Greece
'HR', // Croatia
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
'LT', // Lithuania
'LU', // Luxembourg
'LV', // Latvia
'MT', // Malta
'NL', // Netherlands
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SE', // Sweden
'SI', // Slovenia
'SK', // Slovakia
];
/** EU TAXES */
/** US TAXES */
/** US TAXES */
public string $tax_name1 = '';
public float $tax_rate1 = 0;
public string $tax_name2 = '';
public float $tax_rate2 = 0;
public string $tax_name3 = '';
public float $tax_rate3 = 0;
protected ?Client $client;
protected ?Response $tax_data;
public function __construct()
{
}
public function init(): self
{
return $this;
}
public function setClient(Client $client): self
{
$this->client = $client;
return $this;
}
public function setTaxData(Response $tax_data): self
{
$this->tax_data = $tax_data;
return $this;
}
public function tax($product_tax_type): self
{
return $this;
}
public function taxByType($product_tax_type): self
{
return $this;
}
public function taxReduced(): self
{
return $this;
}
public function taxExempt(): self
{
return $this;
}
public function taxDigital(): self
{
return $this;
}
public function taxService(): self
{
return $this;
}
public function taxShipping(): self
{
return $this;
}
public function taxPhysical(): self
{
return $this;
}
public function default(): self
{
return $this;
}
public function override(): self
{
return $this;
}
public function calculateRates(): self
{
return $this;
}
}

View File

@ -0,0 +1,233 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Tax\DE;
use App\Models\Client;
use App\Models\Product;
use Illuminate\Support\Str;
use App\DataMapper\Tax\BaseRule;
use App\DataMapper\Tax\RuleInterface;
use App\DataMapper\Tax\ZipTax\Response;
class Rule extends BaseRule implements RuleInterface
{
public string $vendor_country_code = 'DE';
public string $client_country_code = 'DE';
public bool $consumer_tax_exempt = false;
public bool $business_tax_exempt = false;
public bool $eu_business_tax_exempt = true;
public bool $foreign_business_tax_exempt = true;
public bool $foreign_consumer_tax_exempt = true;
public string $tax_name1 = '';
public float $tax_rate1 = 0;
public string $tax_name2 = '';
public float $tax_rate2 = 0;
public string $tax_name3 = '';
public float $tax_rate3 = 0;
public float $vat_rate = 0;
public float $reduced_vat_rate = 0;
protected ?Client $client;
protected ?Response $tax_data;
public function __construct()
{
}
public function init(): self
{
$this->client_country_code = $this->client->shipping_country ? $this->client->shipping_country->iso_3166_2 : $this->client->country->iso_3166_2;
$this->calculateRates();
return $this;
}
public function setClient(Client $client): self
{
$this->client = $client;
return $this;
}
public function setTaxData(Response $tax_data): self
{
$this->tax_data = $tax_data;
return $this;
}
//need to add logic here to capture if
public function tax($type): self
{
if ($this->client->is_tax_exempt) {
return $this->taxExempt();
} elseif ($this->client->company->tax_data->regions->EU->tax_all) {
$this->tax_rate1 = $this->vat_rate;
$this->tax_name1 = "MwSt.";
return $this;
}
if ($type)
return $this->taxByType($type);
return $this;
}
public function taxByType($product_tax_type): self
{
if ($this->client->is_tax_exempt) {
return $this->taxExempt();
}
if(!$product_tax_type)
return $this;
match($product_tax_type){
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(),
Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(),
Product::PRODUCT_TYPE_SERVICE => $this->taxService(),
Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(),
Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(),
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(),
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(),
default => $this->default(),
};
return $this;
}
public function taxReduced(): self
{
$this->tax_rate1 = $this->reduced_vat_rate;
$this->tax_name1 = 'ermäßigte MwSt.';
return $this;
}
public function taxExempt(): self
{
$this->tax_name1 = '';
$this->tax_rate1 = 0;
return $this;
}
public function taxDigital(): self
{
$this->tax();
return $this;
}
public function taxService(): self
{
$this->tax();
return $this;
}
public function taxShipping(): self
{
$this->tax();
return $this;
}
public function taxPhysical(): self
{
$this->tax();
return $this;
}
public function default(): self
{
$this->tax_name1 = '';
$this->tax_rate1 = 0;
return $this;
}
public function override(): self
{
return $this;
}
public function calculateRates(): self
{
if ($this->client->is_tax_exempt) {
$this->vat_rate = 0;
$this->reduced_vat_rate = 0;
}
elseif($this->client_country_code != $this->vendor_country_code && in_array($this->client_country_code, $this->eu_country_codes) && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt)
{
$this->vat_rate = 0;
$this->reduced_vat_rate = 0;
// nlog("euro zone and tax exempt");
}
elseif(!in_array(strtoupper($this->client_country_code), $this->eu_country_codes) && ($this->foreign_consumer_tax_exempt || $this->foreign_business_tax_exempt)) //foreign + tax exempt
{
$this->vat_rate = 0;
$this->reduced_vat_rate = 0;
// nlog("foreign and tax exempt");
}
elseif(in_array(strtoupper($this->client_country_code), $this->eu_country_codes) && !$this->client->has_valid_vat_number) //eu country / no valid vat
{
if(($this->vendor_country_code != $this->client_country_code) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold)
{
$this->vat_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->vat_rate;
$this->reduced_vat_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_vat_rate;
// nlog("eu zone with sales above threshold");
}
else {
$this->vat_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->vat_rate;
$this->reduced_vat_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_vat_rate;
// nlog("same eu country with");
}
}
else {
$this->vat_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->vat_rate;
$this->reduced_vat_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_vat_rate;
// nlog("default tax");
}
return $this;
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Tax;
use App\Models\Client;
use App\DataMapper\Tax\ZipTax\Response;
interface RuleInterface
{
public function init();
public function tax(mixed $type);
public function taxByType($type);
public function taxExempt();
public function taxDigital();
public function taxService();
public function taxShipping();
public function taxPhysical();
public function taxReduced();
public function default();
public function override();
public function setClient(Client $client);
public function setTaxData(Response $tax_data);
public function calculateRates();
}

View File

@ -0,0 +1,31 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Tax;
use App\DataMapper\Tax\ZipTax\Response;
/**
* InvoiceTaxData
*
* Definition for the invoice tax data structure
*/
class TaxData
{
public int $updated_at;
public function __construct(public Response $origin)
{
foreach($origin as $key => $value) {
$this->{$key} = $value;
}
}
}

View File

@ -0,0 +1,316 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Tax;
class TaxModel
{
// public string $seller_region = 'US';
public string $seller_subregion = 'CA';
public object $regions;
public function __construct(public ?TaxModel $model = null)
{
if(!$this->model)
$this->regions = $this->init();
else
$this->regions = $model;
}
public function init()
{
$this->regions = new \stdClass();
$this->regions->US = new \stdClass();
$this->regions->EU = new \stdClass();
$this->usRegion()
->euRegion();
return $this->regions;
}
private function usRegion(): self
{
$this->regions->US->has_sales_above_threshold = false;
$this->regions->US->tax_all_subregions = false;
$this->usSubRegions();
return $this;
}
private function euRegion(): self
{
$this->regions->EU->has_sales_above_threshold = false;
$this->regions->EU->tax_all_subregions = false;
$this->regions->EU->vat_threshold = 10000;
$this->euSubRegions();
return $this;
}
private function usSubRegions(): self
{
$this->regions->US->subregions = new \stdClass();
$this->regions->US->subregions->AL = new \stdClass();
$this->regions->US->subregions->AL->apply_tax = false;
$this->regions->US->subregions->AK = new \stdClass();
$this->regions->US->subregions->AK->apply_tax = false;
$this->regions->US->subregions->AZ = new \stdClass();
$this->regions->US->subregions->AZ->apply_tax = false;
$this->regions->US->subregions->AR = new \stdClass();
$this->regions->US->subregions->AR->apply_tax = false;
$this->regions->US->subregions->CA = new \stdClass();
$this->regions->US->subregions->CA->apply_tax = false;
$this->regions->US->subregions->CO = new \stdClass();
$this->regions->US->subregions->CO->apply_tax = false;
$this->regions->US->subregions->CT = new \stdClass();
$this->regions->US->subregions->CT->apply_tax = false;
$this->regions->US->subregions->DE = new \stdClass();
$this->regions->US->subregions->DE->apply_tax = false;
$this->regions->US->subregions->FL = new \stdClass();
$this->regions->US->subregions->FL->apply_tax = false;
$this->regions->US->subregions->GA = new \stdClass();
$this->regions->US->subregions->GA->apply_tax = false;
$this->regions->US->subregions->HI = new \stdClass();
$this->regions->US->subregions->HI->apply_tax = false;
$this->regions->US->subregions->ID = new \stdClass();
$this->regions->US->subregions->ID->apply_tax = false;
$this->regions->US->subregions->IL = new \stdClass();
$this->regions->US->subregions->IL->apply_tax = false;
$this->regions->US->subregions->IN = new \stdClass();
$this->regions->US->subregions->IN->apply_tax = false;
$this->regions->US->subregions->IA = new \stdClass();
$this->regions->US->subregions->IA->apply_tax = false;
$this->regions->US->subregions->KS = new \stdClass();
$this->regions->US->subregions->KS->apply_tax = false;
$this->regions->US->subregions->KY = new \stdClass();
$this->regions->US->subregions->KY->apply_tax = false;
$this->regions->US->subregions->LA = new \stdClass();
$this->regions->US->subregions->LA->apply_tax = false;
$this->regions->US->subregions->ME = new \stdClass();
$this->regions->US->subregions->ME->apply_tax = false;
$this->regions->US->subregions->MD = new \stdClass();
$this->regions->US->subregions->MD->apply_tax = false;
$this->regions->US->subregions->MA = new \stdClass();
$this->regions->US->subregions->MA->apply_tax = false;
$this->regions->US->subregions->MI = new \stdClass();
$this->regions->US->subregions->MI->apply_tax = false;
$this->regions->US->subregions->MN = new \stdClass();
$this->regions->US->subregions->MN->apply_tax = false;
$this->regions->US->subregions->MS = new \stdClass();
$this->regions->US->subregions->MS->apply_tax = false;
$this->regions->US->subregions->MO = new \stdClass();
$this->regions->US->subregions->MO->apply_tax = false;
$this->regions->US->subregions->MT = new \stdClass();
$this->regions->US->subregions->MT->apply_tax = false;
$this->regions->US->subregions->NE = new \stdClass();
$this->regions->US->subregions->NE->apply_tax = false;
$this->regions->US->subregions->NV = new \stdClass();
$this->regions->US->subregions->NV->apply_tax = false;
$this->regions->US->subregions->NH = new \stdClass();
$this->regions->US->subregions->NH->apply_tax = false;
$this->regions->US->subregions->NJ = new \stdClass();
$this->regions->US->subregions->NJ->apply_tax = false;
$this->regions->US->subregions->NM = new \stdClass();
$this->regions->US->subregions->NM->apply_tax = false;
$this->regions->US->subregions->NY = new \stdClass();
$this->regions->US->subregions->NY->apply_tax = false;
$this->regions->US->subregions->NC = new \stdClass();
$this->regions->US->subregions->NC->apply_tax = false;
$this->regions->US->subregions->ND = new \stdClass();
$this->regions->US->subregions->ND->apply_tax = false;
$this->regions->US->subregions->OH = new \stdClass();
$this->regions->US->subregions->OH->apply_tax = false;
$this->regions->US->subregions->OK = new \stdClass();
$this->regions->US->subregions->OK->apply_tax = false;
$this->regions->US->subregions->OR = new \stdClass();
$this->regions->US->subregions->OR->apply_tax = false;
$this->regions->US->subregions->PA = new \stdClass();
$this->regions->US->subregions->PA->apply_tax = false;
$this->regions->US->subregions->RI = new \stdClass();
$this->regions->US->subregions->RI->apply_tax = false;
$this->regions->US->subregions->SC = new \stdClass();
$this->regions->US->subregions->SC->apply_tax = false;
$this->regions->US->subregions->SD = new \stdClass();
$this->regions->US->subregions->SD->apply_tax = false;
$this->regions->US->subregions->TN = new \stdClass();
$this->regions->US->subregions->TN->apply_tax = false;
$this->regions->US->subregions->TX = new \stdClass();
$this->regions->US->subregions->TX->apply_tax = false;
$this->regions->US->subregions->UT = new \stdClass();
$this->regions->US->subregions->UT->apply_tax = false;
$this->regions->US->subregions->VT = new \stdClass();
$this->regions->US->subregions->VT->apply_tax = false;
$this->regions->US->subregions->VA = new \stdClass();
$this->regions->US->subregions->VA->apply_tax = false;
$this->regions->US->subregions->WA = new \stdClass();
$this->regions->US->subregions->WA->apply_tax = false;
$this->regions->US->subregions->WV = new \stdClass();
$this->regions->US->subregions->WV->apply_tax = false;
$this->regions->US->subregions->WI = new \stdClass();
$this->regions->US->subregions->WI->apply_tax = false;
$this->regions->US->subregions->WY = new \stdClass();
$this->regions->US->subregions->WY->apply_tax = false;
return $this;
}
private function euSubRegions(): self
{
$this->regions->EU->subregions = new \stdClass();
$this->regions->EU->subregions->AT = new \stdClass();
$this->regions->EU->subregions->AT->vat_rate = 21;
$this->regions->EU->subregions->AT->reduced_vat_rate = 11;
$this->regions->EU->subregions->AT->apply_tax = false;
$this->regions->EU->subregions->BE = new \stdClass();
$this->regions->EU->subregions->BE->vat_rate = 21;
$this->regions->EU->subregions->BE->reduced_vat_rate = 6;
$this->regions->EU->subregions->BE->apply_tax = false;
$this->regions->EU->subregions->BG = new \stdClass();
$this->regions->EU->subregions->BG->vat_rate = 20;
$this->regions->EU->subregions->BG->reduced_vat_rate = 9;
$this->regions->EU->subregions->BG->apply_tax = false;
$this->regions->EU->subregions->CY = new \stdClass();
$this->regions->EU->subregions->CY->vat_rate = 19;
$this->regions->EU->subregions->CY->reduced_vat_rate = 9;
$this->regions->EU->subregions->CY->apply_tax = false;
$this->regions->EU->subregions->CZ = new \stdClass();
$this->regions->EU->subregions->CZ->vat_rate = 21;
$this->regions->EU->subregions->CZ->reduced_vat_rate = 15;
$this->regions->EU->subregions->CZ->apply_tax = false;
$this->regions->EU->subregions->DE = new \stdClass();
$this->regions->EU->subregions->DE->vat_rate = 19;
$this->regions->EU->subregions->DE->reduced_vat_rate = 7;
$this->regions->EU->subregions->DE->apply_tax = false;
$this->regions->EU->subregions->DK = new \stdClass();
$this->regions->EU->subregions->DK->vat_rate = 25;
$this->regions->EU->subregions->DK->reduced_vat_rate = 0;
$this->regions->EU->subregions->DK->apply_tax = false;
$this->regions->EU->subregions->EE = new \stdClass();
$this->regions->EU->subregions->EE->vat_rate = 20;
$this->regions->EU->subregions->EE->reduced_vat_rate = 9;
$this->regions->EU->subregions->EE->apply_tax = false;
$this->regions->EU->subregions->ES = new \stdClass();
$this->regions->EU->subregions->ES->vat_rate = 21;
$this->regions->EU->subregions->ES->reduced_vat_rate = 10;
$this->regions->EU->subregions->ES->apply_tax = false;
$this->regions->EU->subregions->FI = new \stdClass();
$this->regions->EU->subregions->FI->vat_rate = 24;
$this->regions->EU->subregions->FI->reduced_vat_rate = 14;
$this->regions->EU->subregions->FI->apply_tax = false;
$this->regions->EU->subregions->FR = new \stdClass();
$this->regions->EU->subregions->FR->vat_rate = 20;
$this->regions->EU->subregions->FR->reduced_vat_rate = 5.5;
$this->regions->EU->subregions->FR->apply_tax = false;
// $this->regions->EU->subregions->GB = new \stdClass();
// $this->regions->EU->subregions->GB->vat_rate = 20;
// $this->regions->EU->subregions->GB->reduced_vat_rate = 0;
// $this->regions->EU->subregions->GB->apply_tax = false;
$this->regions->EU->subregions->GR = new \stdClass();
$this->regions->EU->subregions->GR->vat_rate = 24;
$this->regions->EU->subregions->GR->reduced_vat_rate = 13;
$this->regions->EU->subregions->GR->apply_tax = false;
$this->regions->EU->subregions->HR = new \stdClass();
$this->regions->EU->subregions->HR->vat_rate = 25;
$this->regions->EU->subregions->HR->reduced_vat_rate = 5;
$this->regions->EU->subregions->HR->apply_tax = false;
$this->regions->EU->subregions->HU = new \stdClass();
$this->regions->EU->subregions->HU->vat_rate = 27;
$this->regions->EU->subregions->HU->reduced_vat_rate = 5;
$this->regions->EU->subregions->HU->apply_tax = false;
$this->regions->EU->subregions->IE = new \stdClass();
$this->regions->EU->subregions->IE->vat_rate = 23;
$this->regions->EU->subregions->IE->reduced_vat_rate = 0;
$this->regions->EU->subregions->IE->apply_tax = false;
$this->regions->EU->subregions->IT = new \stdClass();
$this->regions->EU->subregions->IT->vat_rate = 22;
$this->regions->EU->subregions->IT->reduced_vat_rate = 10;
$this->regions->EU->subregions->IT->apply_tax = false;
$this->regions->EU->subregions->LT = new \stdClass();
$this->regions->EU->subregions->LT->vat_rate = 21;
$this->regions->EU->subregions->LT->reduced_vat_rate = 9;
$this->regions->EU->subregions->LT->apply_tax = false;
$this->regions->EU->subregions->LU = new \stdClass();
$this->regions->EU->subregions->LU->vat_rate = 17;
$this->regions->EU->subregions->LU->reduced_vat_rate = 3;
$this->regions->EU->subregions->LU->apply_tax = false;
$this->regions->EU->subregions->LV = new \stdClass();
$this->regions->EU->subregions->LV->vat_rate = 21;
$this->regions->EU->subregions->LV->reduced_vat_rate = 12;
$this->regions->EU->subregions->LV->apply_tax = false;
$this->regions->EU->subregions->MT = new \stdClass();
$this->regions->EU->subregions->MT->vat_rate = 18;
$this->regions->EU->subregions->MT->reduced_vat_rate = 5;
$this->regions->EU->subregions->MT->apply_tax = false;
$this->regions->EU->subregions->NL = new \stdClass();
$this->regions->EU->subregions->NL->vat_rate = 21;
$this->regions->EU->subregions->NL->reduced_vat_rate = 9;
$this->regions->EU->subregions->NL->apply_tax = false;
$this->regions->EU->subregions->PT = new \stdClass();
$this->regions->EU->subregions->PT->vat_rate = 23;
$this->regions->EU->subregions->PT->reduced_vat_rate = 6;
$this->regions->EU->subregions->PT->apply_tax = false;
$this->regions->EU->subregions->RO = new \stdClass();
$this->regions->EU->subregions->RO->vat_rate = 19;
$this->regions->EU->subregions->RO->reduced_vat_rate = 5;
$this->regions->EU->subregions->RO->apply_tax = false;
$this->regions->EU->subregions->SE = new \stdClass();
$this->regions->EU->subregions->SE->vat_rate = 25;
$this->regions->EU->subregions->SE->reduced_vat_rate = 12;
$this->regions->EU->subregions->SE->apply_tax = false;
$this->regions->EU->subregions->SI = new \stdClass();
$this->regions->EU->subregions->SI->vat_rate = 22;
$this->regions->EU->subregions->SI->reduced_vat_rate = 9.5;
$this->regions->EU->subregions->SI->apply_tax = false;
$this->regions->EU->subregions->SK = new \stdClass();
$this->regions->EU->subregions->SK->vat_rate = 20;
$this->regions->EU->subregions->SK->reduced_vat_rate = 10;
$this->regions->EU->subregions->SK->apply_tax = false;
return $this;
}
}

View File

@ -0,0 +1,161 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Tax\US;
use App\Models\Client;
use App\Models\Product;
use App\DataMapper\Tax\RuleInterface;
use App\DataMapper\Tax\ZipTax\Response;
class Rule implements RuleInterface
{
public string $tax_name1 = '';
public float $tax_rate1 = 0;
public string $tax_name2 = '';
public float $tax_rate2 = 0;
public string $tax_name3 = '';
public float $tax_rate3 = 0;
public ?Client $client;
public ?Response $tax_data;
public function __construct()
{
}
public function override()
{
return $this;
}
public function setTaxData(Response $tax_data): self
{
$this->tax_data = $tax_data;
return $this;
}
public function setClient(Client $client):self
{
$this->client = $client;
return $this;
}
public function tax($type): self
{
if ($this->client->is_tax_exempt) {
return $this->taxExempt();
}
else if($this->client->company->tax_data->regions->US->tax_all){
$this->tax_rate1 = $this->tax_data->taxSales * 100;
$this->tax_name1 = "{$this->tax_data->geoState} Sales Tax";
return $this;
}
if($type)
return $this->taxByType($type);
return $this;
}
public function taxByType($product_tax_type): self
{
if(!$product_tax_type)
return $this;
match($product_tax_type){
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(),
Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(),
Product::PRODUCT_TYPE_SERVICE => $this->taxService(),
Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(),
Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(),
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(),
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(),
default => $this->default(),
};
return $this;
}
public function taxExempt(): self
{
$this->tax_name1 = '';
$this->tax_rate1 = 0;
return $this;
}
public function taxDigital(): self
{
$this->default();
return $this;
}
public function taxService(): self
{
if($this->tax_data->txbService == 'Y')
$this->default();
return $this;
}
public function taxShipping(): self
{
if($this->tax_data->txbFreight == 'Y')
$this->default();
return $this;
}
public function taxPhysical(): self
{
$this->default();
return $this;
}
public function default(): self
{
$this->tax_rate1 = $this->tax_data->taxSales * 100;
$this->tax_name1 = "{$this->tax_data->geoState} Sales Tax";
return $this;
}
public function taxReduced(): self
{
$this->default();
return $this;
}
public function init(): self
{
return $this;
}
public function calculateRates(): self
{
return $this;
}
}

View File

@ -0,0 +1,108 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Tax\ZipTax;
class Response
{
public string $version = 'v40';
public int $rCode = 100;
/**
* [
* "geoPostalCode" => "92582",
* "geoCity" => "SAN JACINTO",
* "geoCounty" => "RIVERSIDE",
* "geoState" => "CA",
* "taxSales" => 0.0875,
* "taxUse" => 0.0875,
* "txbService" => "N",
* "txbFreight" => "N",
* "stateSalesTax" => 0.06,
* "stateUseTax" => 0.06,
* "citySalesTax" => 0.01,
* "cityUseTax" => 0.01,
* "cityTaxCode" => "874",
* "countySalesTax" => 0.0025,
* "countyUseTax" => 0.0025,
* "countyTaxCode" => "",
* "districtSalesTax" => 0.015,
* "districtUseTax" => 0.015,
* "district1Code" => "26",
* "district1SalesTax" => 0,
* "district1UseTax" => 0,
* "district2Code" => "26",
* "district2SalesTax" => 0.005,
* "district2UseTax" => 0.005,
* "district3Code" => "",
* "district3SalesTax" => 0,
* "district3UseTax" => 0,
* "district4Code" => "33",
* "district4SalesTax" => 0.01,
* "district4UseTax" => 0.01,
* "district5Code" => "",
* "district5SalesTax" => 0,
* "district5UseTax" => 0,
* "originDestination" => "D",
*
* ];
*
*/
public string $seller_region = "";
//US
public string $geoPostalCode = "";
public string $geoCity = "";
public string $geoCounty = "";
public string $geoState = "";
public float $taxSales = 0;
public float $taxUse = 0;
public string $txbService = ""; // N = No, Y = Yes
public string $txbFreight = ""; // N = No, Y = Yes
public float $stateSalesTax = 0;
public float $stateUseTax = 0;
public float $citySalesTax = 0;
public float $cityUseTax = 0;
public string $cityTaxCode = "";
public float $countySalesTax = 0;
public float $countyUseTax = 0;
public string $countyTaxCode = "";
public float $districtSalesTax = 0;
public float $districtUseTax = 0;
public string $district1Code = "";
public float $district1SalesTax = 0;
public float $district1UseTax = 0;
public string $district2Code = "";
public float $district2SalesTax = 0;
public float $district2UseTax = 0;
public string $district3Code = "";
public float $district3SalesTax = 0;
public float $district3UseTax = 0;
public string $district4Code = "";
public float $district4SalesTax = 0;
public float $district4UseTax = 0;
public string $district5Code = "";
public float $district5SalesTax = 0;
public float $district5UseTax = 0;
public string $originDestination = "";
public function __construct($data)
{
foreach($data as $key => $value){
$this->{$key} = $value;
}
}
}

View File

@ -0,0 +1,220 @@
region:
US:
tax_all_subregions: false
seller_subregion: CA
has_sales_above_threshold: false
subregions:
AL:
apply_tax: false
AK:
apply_tax: false
AZ:
apply_tax: false
AR:
apply_tax: false
CA:
apply_tax: false
CO:
apply_tax: false
CT:
apply_tax: false
DE:
apply_tax: false
FL:
apply_tax: false
GA:
apply_tax: false
HI:
apply_tax: false
ID:
apply_tax: false
IL:
apply_tax: false
IN:
apply_tax: false
IA:
apply_tax: false
KS:
apply_tax: false
KY:
apply_tax: false
LA:
apply_tax: false
ME:
apply_tax: false
MD:
apply_tax: false
MA:
apply_tax: false
MI:
apply_tax: false
MN:
apply_tax: false
MS:
apply_tax: false
MO:
apply_tax: false
MT:
apply_tax: false
NE:
apply_tax: false
NV:
apply_tax: false
NH:
apply_tax: false
NJ:
apply_tax: false
NM:
apply_tax: false
NY:
apply_tax: false
NC:
apply_tax: false
ND:
apply_tax: false
OH:
apply_tax: false
OK:
apply_tax: false
OR:
apply_tax: false
PA:
apply_tax: false
RI:
apply_tax: false
SC:
apply_tax: false
SD:
apply_tax: false
TN:
apply_tax: false
TX:
apply_tax: false
UT:
apply_tax: false
VT:
apply_tax: false
VA:
apply_tax: false
WA:
apply_tax: false
WV:
apply_tax: false
WI:
apply_tax: false
WY:
apply_tax: false
EU:
tax_all: false
vat_threshold: 10000
has_sales_above_threshold: false
seller_region: DE
subregions:
AT:
vat: 21
reduced_vat: 11
apply_tax: false
BE:
vat: 21
reduced_vat: 6
apply_tax: false
BG:
vat: 20
reduced_vat: 9
apply_tax: false
CY:
vat: 19
reduced_vat: 9
apply_tax: false
CZ:
vat: 21
reduced_vat: 15
apply_tax: false
DE:
vat: 19
reduced_vat: 7
apply_tax: false
DK:
vat: 25
reduced_vat: 0
apply_tax: false
EE:
vat: 20
reduced_vat: 9
apply_tax: false
ES:
vat: 21
reduced_vat: 10
apply_tax: false
FI:
vat: 24
reduced_vat: 14
apply_tax: false
FR:
vat: 20
reduced_vat: 5.5
apply_tax: false
GB:
vat: 20
reduced_vat: 0
apply_tax: false
GR:
vat: 24
reduced_vat: 13
apply_tax: false
HR:
vat: 25
reduced_vat: 5
apply_tax: false
HU:
vat: 27
reduced_vat: 5
apply_tax: false
IE:
vat: 23
reduced_vat: 0
apply_tax: false
IT:
vat: 22
reduced_vat: 10
apply_tax: false
LT:
vat: 21
reduced_vat: 9
apply_tax: false
LU:
vat: 17
reduced_vat: 3
apply_tax: false
LV:
vat: 21
reduced_vat: 12
apply_tax: false
MT:
vat: 18
reduced_vat: 5
apply_tax: false
NL:
vat: 21
reduced_vat: 9
apply_tax: false
PT:
vat: 23
reduced_vat: 6
apply_tax: false
RO:
vat: 19
reduced_vat: 5
apply_tax: false
SE:
vat: 25
reduced_vat: 12
apply_tax: false
SI:
vat: 22
reduced_vat: 9.5
apply_tax: false
SK:
vat: 20
reduced_vat: 10
apply_tax: false

View File

@ -0,0 +1,24 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataProviders;
class DesignBlocks
{
public function __construct(
public string $includes = '',
public string $header = '',
public string $body = '',
public string $footer = ''
) {
}
}

View File

@ -13,14 +13,18 @@ namespace App\Events\Invoice;
use App\Models\Company;
use App\Models\Invoice;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
/**
* Class InvoiceWasCreated.
*/
class InvoiceWasCreated
class InvoiceWasCreated implements ShouldBroadcast
{
use SerializesModels;
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* @var Invoice
@ -44,4 +48,25 @@ class InvoiceWasCreated
$this->company = $company;
$this->event_vars = $event_vars;
}
/**
* Get the channels the event should broadcast on.
*
* @return PrivateChannel|array
*/
public function broadcastOn()
{
return ['simple-channel'];
}
/**
* Get the data to broadcast.
*
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
return ['id' => 'value'];
}
}

View File

@ -12,8 +12,8 @@
namespace App\Export\CSV;
use App\Models\Client;
use Illuminate\Support\Carbon;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Carbon;
class BaseExport
{
@ -34,7 +34,6 @@ class BaseExport
protected function filterByClients($query)
{
if (isset($this->input['client_id']) && $this->input['client_id'] != 'all') {
$client = Client::withTrashed()->find($this->input['client_id']);
$this->client_description = $client->present()->name;
return $query->where('client_id', $this->input['client_id']);

View File

@ -34,6 +34,8 @@ class InvoiceItemExport extends BaseExport
'amount' => 'amount',
'balance' => 'balance',
'client' => 'client_id',
'client_number' => 'client.number',
'client_id_number' => 'client.id_number',
'custom_surcharge1' => 'custom_surcharge1',
'custom_surcharge2' => 'custom_surcharge2',
'custom_surcharge3' => 'custom_surcharge3',
@ -198,6 +200,8 @@ class InvoiceItemExport extends BaseExport
// if(in_array('client_id', $this->input['report_keys']))
$entity['client'] = $invoice->client->present()->name();
$entity['client_id_number'] = $invoice->client->id_number;
$entity['client_number'] = $invoice->client->number;
// if(in_array('status_id', $this->input['report_keys']))
$entity['status'] = $invoice->stringStatus($invoice->status_id);

View File

@ -11,15 +11,15 @@
namespace App\Export\CSV;
use App\Utils\Ninja;
use League\Csv\Writer;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Product;
use App\Libraries\MultiDB;
use App\Utils\Ninja;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use Illuminate\Database\Eloquent\Collection;
use League\Csv\Writer;
class ProductSalesExport extends BaseExport
{
@ -117,10 +117,9 @@ class ProductSalesExport extends BaseExport
});
$grouped = $this->sales->groupBy('product_key')->map(function ($key, $value){
$grouped = $this->sales->groupBy('product_key')->map(function ($key, $value) {
$data = [
'product' => $value,
'product' => $value,
'quantity' => $key->sum('quantity'),
'markup' => $key->sum('markup'),
'profit' => $key->sum('profit'),
@ -183,11 +182,10 @@ class ProductSalesExport extends BaseExport
$this->sales->push($entity);
return $entity;
}
private function decorateAdvancedFields(Invoice $invoice, $entity) :array
{
{
$product = $this->getProduct($entity['product_key']);
$entity['cost'] = $product->cost ?? 0;
@ -199,31 +197,31 @@ class ProductSalesExport extends BaseExport
$entity['date'] = Carbon::parse($invoice->date)->format($this->company->date_format());
$entity['discount'] = $this->calculateDiscount($invoice, $entity);
$entity['markup'] = round(((($entity['price'] - $entity['discount'] - $entity['cost']) / $unit_cost) * 100),2);
$entity['markup'] = round(((($entity['price'] - $entity['discount'] - $entity['cost']) / $unit_cost) * 100), 2);
$entity['net_total'] = $entity['price'] - $entity['discount'];
$entity['profit'] = $entity['price'] - $entity['discount'] - $entity['cost'];
if(strlen($entity['tax_name1']) > 1) {
if (strlen($entity['tax_name1']) > 1) {
$entity['tax_name1'] = $entity['tax_name1'] . ' [' . $entity['tax_rate1'] . '%]';
$entity['tax_amount1'] = $this->calculateTax($invoice, $entity['line_total'], $entity['tax_rate1']);
}
else
} else {
$entity['tax_amount1'] = 0;
}
if(strlen($entity['tax_name2']) > 1) {
if (strlen($entity['tax_name2']) > 1) {
$entity['tax_name2'] = $entity['tax_name2'] . ' [' . $entity['tax_rate2'] . '%]';
$entity['tax_amount2'] = $this->calculateTax($invoice, $entity['line_total'], $entity['tax_rate2']);
}
else
} else {
$entity['tax_amount2'] = 0;
}
if(strlen($entity['tax_name3']) > 1) {
if (strlen($entity['tax_name3']) > 1) {
$entity['tax_name3'] = $entity['tax_name3'] . ' [' . $entity['tax_rate3'] . '%]';
$entity['tax_amount3'] = $this->calculateTax($invoice, $entity['line_total'], $entity['tax_rate3']);
}
else
} else {
$entity['tax_amount3'] = 0;
}
return $entity;
}
@ -240,13 +238,11 @@ class ProductSalesExport extends BaseExport
{
$amount = $amount - ($amount * ($invoice->discount / 100));
if($invoice->uses_inclusive_taxes) {
if ($invoice->uses_inclusive_taxes) {
return round($amount - ($amount / (1 + ($tax_rate / 100))), 2);
}
else {
} else {
return round(($amount * $tax_rate / 100), 2);
}
}
@ -258,15 +254,15 @@ class ProductSalesExport extends BaseExport
* @param mixed $entity
* @return float
*/
private function calculateDiscount(Invoice $invoice , $entity) :float
private function calculateDiscount(Invoice $invoice, $entity) :float
{
if($entity['discount'] == 0)
if ($entity['discount'] == 0) {
return 0;
if($invoice->is_amount_discount && $entity['discount'] != 0) {
return $entity['discount'];
}
elseif(!$invoice->is_amount_discount && $entity['discount'] != 0) {
if ($invoice->is_amount_discount && $entity['discount'] != 0) {
return $entity['discount'];
} elseif (!$invoice->is_amount_discount && $entity['discount'] != 0) {
return round($entity['line_total'] * ($entity['discount'] / 100), 2);
}

View File

@ -11,6 +11,7 @@
namespace App\Factory;
use App\DataProviders\DesignBlocks;
use App\Models\Design;
class DesignFactory
@ -24,7 +25,7 @@ class DesignFactory
$design->is_active = true;
$design->is_custom = true;
$design->name = '';
$design->design = '';
$design->design = new DesignBlocks();
return $design;
}

View File

@ -35,7 +35,8 @@ class ProductFactory
$product->custom_value3 = '';
$product->custom_value4 = '';
$product->is_deleted = 0;
$product->tax_id = 1;
return $product;
}
}

View File

@ -53,6 +53,7 @@ class RecurringInvoiceFactory
$invoice->remaining_cycles = -1;
$invoice->paid_to_date = 0;
$invoice->auto_bill_enabled = false;
$invoice->is_proforma = false;
$invoice->auto_bill = 'off';
return $invoice;

View File

@ -46,6 +46,7 @@ class RecurringInvoiceToInvoiceFactory
$invoice->custom_value4 = $recurring_invoice->custom_value4;
$invoice->amount = $recurring_invoice->amount;
$invoice->uses_inclusive_taxes = $recurring_invoice->uses_inclusive_taxes;
$invoice->is_proforma = $recurring_invoice->is_proforma;
$invoice->custom_surcharge1 = $recurring_invoice->custom_surcharge1;
$invoice->custom_surcharge2 = $recurring_invoice->custom_surcharge2;

View File

@ -85,7 +85,10 @@ class CreditFilters extends QueryFilters
->orWhere('credits.custom_value1', 'like', '%'.$filter.'%')
->orWhere('credits.custom_value2', 'like', '%'.$filter.'%')
->orWhere('credits.custom_value3', 'like', '%'.$filter.'%')
->orWhere('credits.custom_value4', 'like', '%'.$filter.'%');
->orWhere('credits.custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%');
});
});
}

View File

@ -107,7 +107,10 @@ class InvoiceFilters extends QueryFilters
->orWhere('custom_value1', 'like', '%'.$filter.'%')
->orWhere('custom_value2', 'like', '%'.$filter.'%')
->orWhere('custom_value3', 'like', '%'.$filter.'%')
->orWhere('custom_value4', 'like', '%'.$filter.'%');
->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%');
});
});
}
@ -128,7 +131,7 @@ class InvoiceFilters extends QueryFilters
*/
public function upcoming(): Builder
{
return $this->builder
return $this->builder->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
->where(function ($query) {
$query->whereNull('due_date')
->orWhere('due_date', '>', now());

View File

@ -136,7 +136,7 @@ class PaymentFilters extends QueryFilters
* Sorts the list based on $sort.
*
* formatted as column|asc
*
*
* @param string $sort
* @return Builder
*/

View File

@ -63,8 +63,8 @@ class PurchaseOrderFilters extends QueryFilters
$po_status[] = PurchaseOrder::STATUS_CANCELLED;
}
if (count($status_parameters) >=1) {
$query->whereIn('status_id', $status_parameters);
if (count($po_status) >=1) {
$query->whereIn('status_id', $po_status);
}
});
@ -93,7 +93,10 @@ class PurchaseOrderFilters extends QueryFilters
->orWhere('custom_value1', 'like', '%'.$filter.'%')
->orWhere('custom_value2', 'like', '%'.$filter.'%')
->orWhere('custom_value3', 'like', '%'.$filter.'%')
->orWhere('custom_value4', 'like', '%'.$filter.'%');
->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('vendor', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%');
});
});
}

View File

@ -37,7 +37,10 @@ class QuoteFilters extends QueryFilters
->orwhere('custom_value1', 'like', '%'.$filter.'%')
->orWhere('custom_value2', 'like', '%'.$filter.'%')
->orWhere('custom_value3', 'like', '%'.$filter.'%')
->orWhere('custom_value4', 'like', '%'.$filter.'%');
->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%');
});
});
}

View File

@ -11,10 +11,13 @@
namespace App\Helpers\Invoice;
use App\DataMapper\BaseSettings;
use App\DataMapper\InvoiceItem;
use App\Models\Client;
use App\Models\Invoice;
use App\DataMapper\InvoiceItem;
use App\DataMapper\BaseSettings;
use App\DataMapper\Tax\RuleInterface;
use App\Utils\Traits\NumberFormatter;
use App\DataMapper\Tax\ZipTax\Response;
class InvoiceItemSum
{
@ -22,6 +25,38 @@ class InvoiceItemSum
use Discounter;
use Taxer;
private array $tax_jurisdictions = [
'AT', // Austria
'BE', // Belgium
'BG', // Bulgaria
'CY', // Cyprus
'CZ', // Czech Republic
'DE', // Germany
'DK', // Denmark
'EE', // Estonia
'ES', // Spain
'FI', // Finland
'FR', // France
'GR', // Greece
'HR', // Croatia
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
'LT', // Lithuania
'LU', // Luxembourg
'LV', // Latvia
'MT', // Malta
'NL', // Netherlands
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SE', // Sweden
'SI', // Slovenia
'SK', // Slovakia
'US', //USA
];
protected $invoice;
private $items;
@ -48,6 +83,12 @@ class InvoiceItemSum
private $tax_collection;
private ?Client $client;
private bool $calc_tax = false;
private RuleInterface $rule;
public function __construct($invoice)
{
$this->tax_collection = collect([]);
@ -56,6 +97,8 @@ class InvoiceItemSum
if ($this->invoice->client) {
$this->currency = $this->invoice->client->currency();
$this->client = $this->invoice->client;
$this->shouldCalculateTax();
} else {
$this->currency = $this->invoice->vendor->currency();
}
@ -89,6 +132,33 @@ class InvoiceItemSum
return $this;
}
private function shouldCalculateTax(): self
{
if (!$this->invoice->company->calculate_taxes) {
$this->calc_tax = false;
return $this;
}
//should we be filtering by client country here? do we need to reflect at the company <=> client level?
if (in_array($this->client->country->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions
$class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule";
$tax_data = new Response($this->invoice->tax_data);
$this->rule = new $class();
$this->rule
->setTaxData($tax_data)
->setClient($this->client)
->init();
$this->calc_tax = true;
return $this;
}
return $this;
}
private function push()
{
$this->sub_total += $this->getLineTotal();
@ -122,8 +192,33 @@ class InvoiceItemSum
return $this;
}
/**
* Attempts to calculate taxes based on the clients location
*
* @return self
*/
private function calcTaxesAutomatically(): self
{
$this->rule->tax($this->item->tax_id ?? null);
$this->item->tax_name1 = $this->rule->tax_name1;
$this->item->tax_rate1 = $this->rule->tax_rate1;
$this->item->tax_name2 = $this->rule->tax_name2;
$this->item->tax_rate2 = $this->rule->tax_rate2;
$this->item->tax_name3 = $this->rule->tax_name3;
$this->item->tax_rate3 = $this->rule->tax_rate3;
return $this;
}
private function calcTaxes()
{
if ($this->calc_tax) {
$this->calcTaxesAutomatically();
}
$item_tax = 0;
$amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / 100));
@ -131,7 +226,6 @@ class InvoiceItemSum
$item_tax += $item_tax_rate1_total;
// if($item_tax_rate1_total != 0)
if (strlen($this->item->tax_name1) > 1) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
}
@ -155,7 +249,7 @@ class InvoiceItemSum
$this->setTotalTaxes($this->formatValue($item_tax, $this->currency->precision));
$this->item->gross_line_total = $this->getLineTotal() + $item_tax;
$this->item->tax_amount = $item_tax;
return $this;

View File

@ -45,6 +45,8 @@ class InvoiceSum
private $precision;
public InvoiceItemSum $invoice_items;
/**
* Constructs the object with Invoice and Settings object.
*

View File

@ -11,16 +11,17 @@
namespace App\Http\Controllers;
use App\Http\Requests\Account\CreateAccountRequest;
use App\Http\Requests\Account\UpdateAccountRequest;
use App\Jobs\Account\CreateAccount;
use App\Models\Account;
use App\Libraries\MultiDB;
use App\Utils\TruthSource;
use App\Models\CompanyUser;
use Illuminate\Http\Response;
use App\Jobs\Account\CreateAccount;
use App\Transformers\AccountTransformer;
use App\Transformers\CompanyUserTransformer;
use App\Utils\TruthSource;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Response;
use App\Http\Requests\Account\CreateAccountRequest;
use App\Http\Requests\Account\UpdateAccountRequest;
class AccountController extends BaseController
{
@ -146,15 +147,20 @@ class AccountController extends BaseController
if (! ($account instanceof Account)) {
return $account;
}
MultiDB::findAndSetDbByAccountKey($account->key);
$ct = CompanyUser::whereUserId(auth()->user()->id);
$cu = CompanyUser::where('user_id', $account->users()->first()->id);
$company_user = $cu->first();
$truth = app()->make(TruthSource::class);
$truth->setCompanyUser($ct->first());
$truth->setUser(auth()->user());
$truth->setCompany($ct->first()->company);
$truth->setCompanyUser($company_user);
$truth->setUser($company_user->user);
$truth->setCompany($company_user->company);
$truth->setCompanyToken($company_user->tokens()->where('user_id', $company_user->user_id)->where('company_id', $company_user->company_id)->first());
return $this->listResponse($ct);
return $this->listResponse($cu);
}
public function update(UpdateAccountRequest $request, Account $account)

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -15,7 +16,6 @@ use App\DataMapper\Analytics\LoginFailure;
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;
@ -23,8 +23,6 @@ use App\Libraries\MultiDB;
use App\Libraries\OAuth\OAuth;
use App\Libraries\OAuth\Providers\Google;
use App\Models\Account;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\CompanyUser;
use App\Models\User;
@ -38,7 +36,6 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Laravel\Socialite\Facades\Socialite;
use Microsoft\Graph\Model;
use PragmaRX\Google2FA\Google2FA;
@ -46,18 +43,7 @@ use Turbo124\Beacon\Facades\LightLogs;
class LoginController extends BaseController
{
/**
* @OA\Tag(
* name="login",
* description="Authentication",
* @OA\ExternalDocumentation(
* description="Find out more",
* url="https://invoiceninja.github.io"
* )
* )
*/
use AuthenticatesUsers;
use UserSessionAttributes;
use LoginCache;
@ -89,7 +75,7 @@ class LoginController extends BaseController
* @param Request $request
* @param User $user
* @return void
* deprecated .1 API ONLY we don't need to set any session variables
* @deprecated .1 API ONLY we don't need to set any session variables
*/
public function authenticated(Request $request, User $user): void
{
@ -99,63 +85,8 @@ class LoginController extends BaseController
/**
* Login via API.
*
* @param Request $request The request
*
* @return Response|User Process user login.
*
* @param LoginRequest $request The request
* @throws \Illuminate\Validation\ValidationException
* @OA\Post(
* path="/api/v1/login",
* operationId="postLogin",
* tags={"login"},
* summary="Attempts authentication",
* description="Returns a CompanyUser object on success",
* @OA\Parameter(ref="#/components/parameters/X-API-SECRET"),
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(ref="#/components/parameters/include_static"),
* @OA\Parameter(ref="#/components/parameters/clear_cache"),
* @OA\RequestBody(
* description="User credentials",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="object",
* @OA\Property(
* property="email",
* description="The user email address",
* type="string",
* ),
* @OA\Property(
* property="password",
* example="1234567",
* description="The user password must meet minimum criteria ~ >6 characters",
* type="string"
* )
* )
* )
* ),
* @OA\Response(
* response=200,
* description="The Company User response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/CompanyUser"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function apiLogin(LoginRequest $request)
{
@ -175,7 +106,7 @@ class LoginController extends BaseController
if ($this->attemptLogin($request)) {
LightLogs::create(new LoginSuccess())
->increment()
->queue();
->batch();
$user = $this->guard()->user();
@ -221,7 +152,7 @@ class LoginController extends BaseController
} else {
LightLogs::create(new LoginFailure())
->increment()
->queue();
->batch();
$this->incrementLoginAttempts($request);
@ -236,39 +167,7 @@ class LoginController extends BaseController
* Refreshes the data feed with the current Company User.
*
* @param Request $request
* @return CompanyUser Refresh Feed.
*
*
* @OA\Post(
* path="/api/v1/refresh",
* operationId="refresh",
* tags={"refresh"},
* summary="Refreshes the dataset",
* description="Refreshes the dataset",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(ref="#/components/parameters/include_static"),
* @OA\Parameter(ref="#/components/parameters/clear_cache"),
* @OA\Response(
* response=200,
* description="The Company User response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/CompanyUser"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
* @return CompanyUser Refresh Feed.
*/
public function refresh(Request $request)
{
@ -346,7 +245,7 @@ class LoginController extends BaseController
private function handleSocialiteLogin($provider, $token)
{
$user = $this->getSocialiteUser($provider, $token);
if ($user) {
return $this->loginOrCreateFromSocialite($user, $provider);
}
@ -363,7 +262,7 @@ class LoginController extends BaseController
'oauth_user_id' => $user->id,
'oauth_provider_id' => $provider,
];
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);
@ -408,7 +307,7 @@ class LoginController extends BaseController
return $this->timeConstrainedResponse($cu);
}
nlog("socialite");
nlog($user);
@ -478,7 +377,7 @@ class LoginController extends BaseController
if (auth()->user()->company_users()->count() != auth()->user()->tokens()->distinct('company_id')->count()) {
auth()->user()->companies->each(function ($company) {
if (!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->exists()) {
if (!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->where('is_system', true)->exists()) {
(new CreateCompanyToken($company, auth()->user(), 'Google_O_Auth'))->handle();
}
});
@ -499,7 +398,6 @@ class LoginController extends BaseController
return response()->json(['message' => 'Invalid response from oauth server, no access token in response.'], 400);
}
$graph = new \Microsoft\Graph\Graph();
$graph->setAccessToken($accessToken);
@ -536,17 +434,22 @@ class LoginController extends BaseController
return $this->existingLoginUser($user->getId(), 'microsoft');
}
// Signup!
$new_account = [
'first_name' => $user->getGivenName() ?: '',
'last_name' => $user->getSurname() ?: '',
'password' => '',
'email' => $email,
'oauth_user_id' => $user->getId(),
'oauth_provider_id' => 'microsoft',
];
return $this->createNewAccount($new_account);
// Signup!
if (request()->has('create') && request()->input('create') == 'true') {
$new_account = [
'first_name' => $user->getGivenName() ?: '',
'last_name' => $user->getSurname() ?: '',
'password' => '',
'email' => $email,
'oauth_user_id' => $user->getId(),
'oauth_provider_id' => 'microsoft',
];
return $this->createNewAccount($new_account);
}
return response()->json(['message' => 'User not found. If you believe this is an error, please send an email to contact@invoiceninja.com'], 400);
}
@ -640,19 +543,23 @@ class LoginController extends BaseController
return $this->existingLoginUser($google->harvestSubField($user), 'google');
}
//user not found anywhere - lets sign them up.
$name = OAuth::splitName($google->harvestName($user));
if (request()->has('create') && request()->input('create') == 'true') {
//user not found anywhere - lets sign them up.
$name = OAuth::splitName($google->harvestName($user));
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id' => 'google',
];
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id' => 'google',
];
return $this->createNewAccount($new_account);
return $this->createNewAccount($new_account);
}
return response()->json(['message' => 'User not found. If you believe this is an error, please send an email to contact@invoiceninja.com'], 400);
}
return response()
@ -700,7 +607,7 @@ class LoginController extends BaseController
if ($provider == 'microsoft') {
$scopes = ['email', 'Mail.Send', 'offline_access', 'profile', 'User.Read openid'];
$parameters = ['response_type' => 'code', 'redirect_uri' => config('ninja.app_url')."/auth/microsoft"];
$parameters = ['response_type' => 'code', 'redirect_uri' => config('ninja.app_url') . "/auth/microsoft"];
}
if (request()->has('code')) {

View File

@ -1,81 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'first_name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\User
*/
protected function create(array $data)
{
return User::create([
'first_name' => $data['first_name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
}

View File

@ -28,16 +28,13 @@ class VendorContactLoginController extends Controller
public function catch()
{
$data = [
];
return $this->render('purchase_orders.catch');
}
public function logout()
{
Auth::guard('vendor')->logout();
request()->session()->invalidate();
return redirect('/vendors');

View File

@ -1,50 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Auth;
use Illuminate\Foundation\Auth\VerifiesEmails;
use Illuminate\Routing\Controller;
class VerificationController extends Controller
{
/*
|--------------------------------------------------------------------------
| Email Verification Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling email verification for any
| user that recently registered with the application. Emails may also
| be resent if the user did not receive the original email message.
|
*/
use VerifiesEmails;
/**
* Where to redirect users after verification.
*
* @var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
}

View File

@ -276,8 +276,6 @@ class YodleeController extends BaseController
{
//this is the main hook we use for notifications
nlog("data refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);

View File

@ -531,7 +531,6 @@ class BankTransactionController extends BaseController
*/
public function match(MatchBankTransactionRequest $request)
{
$bts = (new MatchBankTransactions(auth()->user()->company()->id, auth()->user()->company()->db, $request->all()))->handle();
return $this->listResponse($bts);

View File

@ -33,6 +33,7 @@ use App\Utils\Statics;
use App\Utils\Traits\AppSetup;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use League\Fractal\Manager;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@ -565,7 +566,7 @@ class BaseController extends Controller
/**
* Mini Load Query
*
* @param mixed $query
* @param Builder $query
* @return void
*/
protected function miniLoadResponse($query)
@ -667,7 +668,7 @@ class BaseController extends Controller
/**
* Passes back the miniloaded data response
*
* @param mixed $query
* @param Builder $query
* @return void
*/
protected function timeConstrainedResponse($query)
@ -911,9 +912,8 @@ class BaseController extends Controller
/**
* List response
*
* @param mixed $query
* @return void
*
* @param Builder $query
*/
protected function listResponse($query)
{
@ -927,24 +927,26 @@ class BaseController extends Controller
$query->with($includes);
if (auth()->user() && ! auth()->user()->hasPermission('view_'.Str::snake(class_basename($this->entity_type)))) {
$user = Auth::user();
if ($user && ! $user->hasPermission('view_'.Str::snake(class_basename($this->entity_type)))) {
if (in_array($this->entity_type, [User::class])) {
$query->where('id', auth()->user()->id);
$query->where('id', $user->id);
} elseif (in_array($this->entity_type, [BankTransactionRule::class,CompanyGateway::class, TaxRate::class, BankIntegration::class, Scheduler::class, BankTransaction::class, Webhook::class, ExpenseCategory::class])) { //table without assigned_user_id
if ($this->entity_type == BankIntegration::class && !auth()->user()->isSuperUser() && auth()->user()->hasIntersectPermissions(['create_bank_transaction','edit_bank_transaction','view_bank_transaction'])) {
if ($this->entity_type == BankIntegration::class && !$user->isSuperUser() && $user->hasIntersectPermissions(['create_bank_transaction','edit_bank_transaction','view_bank_transaction'])) {
$query->exclude(["balance"]);
} //allows us to selective display bank integrations back to the user if they can view / create bank transactions but without the bank balance being present in the response
else {
$query->where('user_id', '=', auth()->user()->id);
$query->where('user_id', '=', $user->id);
}
} elseif (in_array($this->entity_type, [Design::class, GroupSetting::class, PaymentTerm::class, TaskStatus::class])) {
// nlog($this->entity_type);
} else {
$query->where('user_id', '=', auth()->user()->id)->orWhere('assigned_user_id', auth()->user()->id);
$query->where('user_id', '=', $user->id)->orWhere('assigned_user_id', $user->id);
}
}
if ($this->entity_type == Client::class && auth()->user()->hasExcludedPermissions($this->client_excludable_permissions, $this->client_excludable_overrides)) {
if ($this->entity_type == Client::class && $user->hasExcludedPermissions($this->client_excludable_permissions, $this->client_excludable_overrides)) {
$query->exclude($this->client_exclusion_fields);
}
@ -1010,7 +1012,6 @@ class BaseController extends Controller
* Item Response
*
* @param mixed $item
* @return void
*/
protected function itemResponse($item)
{
@ -1087,7 +1088,7 @@ class BaseController extends Controller
{
if ((bool) $this->checkAppSetup() !== false && $account = Account::first()) {
//always redirect invoicing.co to invoicing.co
if (Ninja::isHosted() && !in_array(request()->getSchemeAndHttpHost(), ['https://staging.invoicing.co', 'https://invoicing.co', 'https://demo.invoicing.co'])) {
if (Ninja::isHosted() && !in_array(request()->getSchemeAndHttpHost(), ['https://staging.invoicing.co', 'https://invoicing.co', 'https://demo.invoicing.co', 'https://invoiceninja.net'])) {
return redirect()->secure('https://invoicing.co');
}

View File

@ -80,8 +80,6 @@ class InvoiceController extends Controller
/**
* Pay one or more invoices.
*
* @param ProcessInvoicesInBulkRequest $request
* @return mixed
*/
public function catch_bulk()
{

View File

@ -12,24 +12,24 @@
namespace App\Http\Controllers\ClientPortal;
use App\Factory\PaymentFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment;
use Illuminate\View\View;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use Illuminate\Http\Request;
use App\Models\CompanyGateway;
use App\Factory\PaymentFactory;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesDates;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Contracts\View\Factory;
use App\PaymentDrivers\Stripe\BankTransfer;
use App\Services\ClientPortal\InstantPayment;
use App\Services\Subscription\SubscriptionService;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
/**
* Class PaymentController.
@ -64,15 +64,14 @@ class PaymentController extends Controller
$data = false;
$gateway = false;
if($payment->gateway_type_id == GatewayType::DIRECT_DEBIT && $payment->type_id == PaymentType::DIRECT_DEBIT){
if ($payment->gateway_type_id == GatewayType::DIRECT_DEBIT && $payment->type_id == PaymentType::DIRECT_DEBIT) {
if (method_exists($payment->company_gateway->driver($payment->client), 'getPaymentIntent')) {
$stripe = $payment->company_gateway->driver($payment->client);
$payment_intent = $stripe->getPaymentIntent($payment->transaction_reference);
$bt = new BankTransfer($stripe);
match($payment->currency->code){
match ($payment->currency->code) {
'MXN' => $data = $bt->formatDataforMx($payment_intent),
'EUR' => $data = $bt->formatDataforEur($payment_intent),
'JPY' => $data = $bt->formatDataforJp($payment_intent),
@ -87,7 +86,7 @@ class PaymentController extends Controller
return $this->render('payments.show', [
'payment' => $payment,
'bank_details' => $payment_intent ? $data : false,
'currency' => strtolower($payment->currency->code),
'currency' => $payment->currency ? strtolower($payment->currency->code) : strtolower($payment->client->currency()->code),
]);
}

View File

@ -15,7 +15,6 @@ namespace App\Http\Controllers\ClientPortal;
use App\Events\Payment\Methods\MethodDeleted;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\PaymentMethod\CreatePaymentMethodRequest;
use App\Http\Requests\ClientPortal\PaymentMethod\VerifyPaymentMethodRequest;
use App\Http\Requests\Request;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;

View File

@ -0,0 +1,120 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\ClientPortal;
use App\DataMapper\InvoiceItem;
use App\Factory\InvoiceFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\PrePayments\StorePrePaymentRequest;
use App\Repositories\InvoiceRepository;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
/**
* Class PrePaymentController.
*/
class PrePaymentController extends Controller
{
use MakesHash;
use MakesDates;
/**
* Show the list of payments.
*
* @return Factory|View
*/
public function index()
{
$client = auth()->guard('contact')->user()->client;
$minimum = $client->getSetting('client_initiated_payments_minimum');
$minimum_amount = $minimum == 0 ? "" : Number::formatMoney($minimum, $client);
$data = [
'title' => ctrans('texts.amount'). " " .$client->currency()->code." (".auth()->guard('contact')->user()->client->currency()->symbol . ")",
'allows_recurring' => true,
'minimum' => $minimum,
'minimum_amount' => $minimum_amount,
];
return $this->render('pre_payments.index', $data);
}
public function process(StorePrePaymentRequest $request)
{
$invoice = InvoiceFactory::create(auth()->guard('contact')->user()->company_id, auth()->guard('contact')->user()->user_id);
$invoice->due_date = now()->format('Y-m-d');
$invoice->is_proforma = true;
$invoice->client_id = auth()->guard('contact')->user()->client_id;
$line_item = new InvoiceItem();
$line_item->cost = (float)$request->amount;
$line_item->quantity = 1;
$line_item->product_key = ctrans('texts.pre_payment');
$line_item->notes = $request->notes;
$line_item->type_id = '1';
$items = [];
$items[] = $line_item;
$invoice->line_items = $items;
$invoice->number = ctrans('texts.pre_payment') . " " . now()->format('Y-m-d : H:i:s');
$invoice_repo = new InvoiceRepository();
$data = [
'client_id' => $invoice->client_id,
'quantity' => 1,
'date' => now()->format('Y-m-d'),
];
$invoice = $invoice_repo->save($data, $invoice)
->service()
->markSent()
->applyNumber()
->fillDefaults()
->save();
$total = $invoice->balance;
//format totals
$formatted_total = Number::formatMoney($invoice->amount, auth()->guard('contact')->user()->client);
$payment_methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods($request->amount);
//if there is only one payment method -> lets return straight to the payment page
$invoices = collect();
$invoices->push($invoice);
$invoices->map(function ($invoice) {
$invoice->balance = Number::formatValue($invoice->balance, $invoice->client->currency());
return $invoice;
});
$data = [
'settings' => auth()->guard('contact')->user()->client->getMergedSettings(),
'invoices' => $invoices,
'formatted_total' => $formatted_total,
'payment_methods' => $payment_methods,
'hashed_ids' => $invoices->pluck('hashed_id'),
'total' => $total,
'pre_payment' => true,
'frequency_id' => $request->frequency_id,
'remaining_cycles' => $request->remaining_cycles,
'is_recurring' => $request->is_recurring == 'on' ? true : false,
];
return $this->render('invoices.payment', $data);
}
}

View File

@ -178,7 +178,7 @@ class QuoteController extends Controller
if ($process) {
foreach ($quotes as $quote) {
if (request()->has('user_input') && strlen(request()->input('user_input')) > 2) {
$quote->public_notes .= $quote->public_notes . "\n" . request()->input('user_input');
$quote->po_number = substr(request()->input('user_input'), 0, 180);
$quote->saveQuietly();
}

View File

@ -19,22 +19,28 @@ use App\Utils\Ninja;
class SubscriptionController extends Controller
{
/**
* This function is used to display the subscription page.
*
* @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory
*/
public function index()
{
if (Ninja::isHosted()) {
$count = RecurringInvoice::query()
->where('client_id', auth()->guard('contact')->user()->client->id)
->where('company_id', auth()->guard('contact')->user()->client->company_id)
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('is_deleted', 0)
->whereNotNull('subscription_id')
->withTrashed()
->count();
// if (Ninja::isHosted()) {
// $count = RecurringInvoice::query()
// ->where('client_id', auth()->guard('contact')->user()->client->id)
// ->where('company_id', auth()->guard('contact')->user()->client->company_id)
// ->where('status_id', RecurringInvoice::STATUS_ACTIVE)
// ->where('is_deleted', 0)
// ->whereNotNull('subscription_id')
// ->withTrashed()
// ->count();
if ($count == 0) {
return redirect()->route('client.ninja_contact_login', ['contact_key' => auth()->guard('contact')->user()->contact_key, 'company_key' => auth()->guard('contact')->user()->company->company_key]);
}
}
// if ($count == 0) {
// return redirect()->route('client.ninja_contact_login', ['contact_key' => auth()->guard('contact')->user()->contact_key, 'company_key' => auth()->guard('contact')->user()->company->company_key]);
// }
// }
return render('subscriptions.index');
}
@ -44,7 +50,6 @@ class SubscriptionController extends Controller
*
* @param ShowRecurringInvoiceRequest $request
* @param RecurringInvoice $recurring_invoice
* @return Factory|View
*/
public function show(ShowRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice)
{

View File

@ -44,7 +44,6 @@ class SubscriptionPurchaseController extends Controller
public function upgrade(Subscription $subscription, Request $request)
{
App::setLocale($subscription->company->locale());
/* Make sure the contact is logged into the correct company for this subscription */

View File

@ -40,6 +40,7 @@ use App\Utils\Traits\SavesDocuments;
use App\Utils\Traits\Uploadable;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use Turbo124\Beacon\Facades\LightLogs;
/**
@ -486,6 +487,11 @@ class CompanyController extends BaseController
$company_user->forceDelete();
});
try {
Storage::disk(config('filesystems.default'))->deleteDirectory($company->company_key);
} catch(\Exception $e) {
}
$account->delete();
if (Ninja::isHosted()) {
@ -494,7 +500,7 @@ class CompanyController extends BaseController
LightLogs::create(new AccountDeleted())
->increment()
->queue();
->batch();
} else {
$company_id = $company->id;
@ -511,6 +517,12 @@ class CompanyController extends BaseController
$nmo->to_user = auth()->user();
(new NinjaMailerJob($nmo, true))->handle();
try {
Storage::disk(config('filesystems.default'))->deleteDirectory($company->company_key);
} catch(\Exception $e) {
}
$company->delete();
//If we are deleting the default companies, we'll need to make a new company the default.

View File

@ -11,26 +11,27 @@
namespace App\Http\Controllers;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Events\Credit\CreditWasEmailed;
use App\Events\Quote\QuoteWasEmailed;
use App\Http\Requests\Email\SendEmailRequest;
use App\Jobs\Entity\EmailEntity;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Services\Email\Email;
use Illuminate\Http\Response;
use App\Utils\Traits\MakesHash;
use App\Jobs\Entity\EmailEntity;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Services\Email\Email;
use App\Services\Email\EmailObject;
use App\Events\Quote\QuoteWasEmailed;
use App\Transformers\QuoteTransformer;
use App\Events\Credit\CreditWasEmailed;
use App\Transformers\CreditTransformer;
use App\Transformers\InvoiceTransformer;
use App\Http\Requests\Email\SendEmailRequest;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\QuoteTransformer;
use App\Transformers\RecurringInvoiceTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Response;
use Illuminate\Mail\Mailables\Address;
class EmailController extends BaseController
{
@ -135,6 +136,9 @@ class EmailController extends BaseController
$mo->email_template_body = $request->input('template');
$mo->email_template_subject = str_replace("template", "subject", $request->input('template'));
if ($request->has('cc_email') && $request->cc_email) {
$mo->cc[] = new Address($request->cc_email);
}
// if ($entity == 'purchaseOrder' || $entity == 'purchase_order' || $template == 'purchase_order' || $entity == 'App\Models\PurchaseOrder') {
// return $this->sendPurchaseOrder($entity_obj, $data, $template);
@ -149,7 +153,6 @@ class EmailController extends BaseController
$mo->invitation_id = $invitation->id;
Email::dispatch($mo, $invitation->company);
}
});
@ -190,7 +193,7 @@ class EmailController extends BaseController
$this->entity_transformer = RecurringInvoiceTransformer::class;
}
if($entity_obj instanceof PurchaseOrder){
if ($entity_obj instanceof PurchaseOrder) {
$this->entity_type = PurchaseOrder::class;
$this->entity_transformer = PurchaseOrderTransformer::class;
}
@ -214,8 +217,7 @@ class EmailController extends BaseController
private function resolveClass(string $entity): string
{
match($entity){
match ($entity) {
'invoice' => $class = Invoice::class,
'App\Models\Invoice' => $class = Invoice::class,
'credit' => $class = Credit::class,
@ -229,6 +231,5 @@ class EmailController extends BaseController
};
return $class;
}
}

View File

@ -38,6 +38,8 @@ class HostedMigrationController extends Controller
$account->hosted_company_count = 10;
$account->save();
MultiDB::findAndSetDbByAccountKey($account->key);
$company = $account->companies->first();
$company_token = CompanyToken::where('user_id', auth()->user()->id)

View File

@ -163,19 +163,15 @@ class ImportController extends Controller
$bestDelimiter = ' ';
$count = 0;
foreach ($delimiters as $delimiter) {
// if (substr_count($csvfile, $delimiter) > $count) {
// $count = substr_count($csvfile, $delimiter);
// $bestDelimiter = $delimiter;
// }
if (substr_count(strstr($csvfile,"\n",true), $delimiter) > $count) {
if (substr_count(strstr($csvfile, "\n", true), $delimiter) > $count) {
$count = substr_count($csvfile, $delimiter);
$bestDelimiter = $delimiter;
}
}
return $bestDelimiter;
}

View File

@ -11,13 +11,13 @@
namespace App\Http\Controllers;
use stdClass;
use Carbon\Carbon;
use App\Models\Account;
use App\Utils\CurlUtils;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Request;
use stdClass;
class LicenseController extends BaseController
{
@ -80,7 +80,7 @@ class LicenseController extends BaseController
* ),
* )
*/
public function indexx()
public function index()
{
$this->checkLicense();
@ -89,6 +89,10 @@ class LicenseController extends BaseController
$license_key = request()->input('license_key');
$product_id = 3;
if(substr($license_key, 0, 3) == 'v5_') {
return $this->v5ClaimLicense($license_key, $product_id);
}
$url = config('ninja.license_url')."/claim_license?license_key={$license_key}&product_id={$product_id}&get_date=true";
$data = trim(CurlUtils::get($url));
@ -149,21 +153,19 @@ class LicenseController extends BaseController
return response()->json($error, 400);
}
public function v5ClaimLicense(Request $request)
public function v5ClaimLicense(string $license_key)
{
$this->checkLicense();
/* Catch claim license requests */
if (config('ninja.environment') == 'selfhost' && request()->has('license_key')) {
if (config('ninja.environment') == 'selfhost') {
// $response = Http::get( "http://ninja.test:8000/claim_license", [
$response = Http::get( "https://invoicing.co/claim_license", [
'license_key' => $request->input('license_key'),
$response = Http::get("https://invoicing.co/claim_license", [
'license_key' => $license_key,
'product_id' => 3,
]);
if($response->successful()) {
if ($response->successful()) {
$payload = $response->json();
$account = auth()->user()->account;
@ -179,8 +181,7 @@ class LicenseController extends BaseController
];
return response()->json($error, 200);
}else {
} else {
$error = [
'message' => trans('texts.white_label_license_error'),
'errors' => new stdClass,
@ -188,7 +189,6 @@ class LicenseController extends BaseController
return response()->json($error, 400);
}
}
$error = [
@ -197,7 +197,6 @@ class LicenseController extends BaseController
];
return response()->json($error, 400);
}
@ -210,6 +209,5 @@ class LicenseController extends BaseController
$account->plan_expires = null;
$account->save();
}
}
}

View File

@ -23,7 +23,6 @@ use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\GroupSetting;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Quote;
@ -37,7 +36,6 @@ use App\Services\PdfMaker\Design;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker;
use App\Services\Preview\StubBuilder;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
@ -146,7 +144,7 @@ class PreviewController extends BaseController
$maker
->design($design)
->build();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}

View File

@ -16,6 +16,7 @@ use App\Events\RecurringInvoice\RecurringInvoiceWasUpdated;
use App\Factory\RecurringInvoiceFactory;
use App\Filters\RecurringInvoiceFilters;
use App\Http\Requests\RecurringInvoice\ActionRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\BulkRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\CreateRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\DestroyRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\EditRecurringInvoiceRequest;
@ -23,6 +24,7 @@ use App\Http\Requests\RecurringInvoice\ShowRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\StoreRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\UpdateRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\UploadRecurringInvoiceRequest;
use App\Jobs\RecurringInvoice\UpdateRecurring;
use App\Models\Account;
use App\Models\RecurringInvoice;
use App\Repositories\RecurringInvoiceRepository;
@ -392,50 +394,6 @@ class RecurringInvoiceController extends BaseController
*
* @param DestroyRecurringInvoiceRequest $request
* @param RecurringInvoice $recurring_invoice
*
* @return Response
*
*
* @throws \Exception
* @OA\Delete(
* path="/api/v1/recurring_invoices/{id}",
* operationId="deleteRecurringInvoice",
* tags={"recurring_invoices"},
* summary="Deletes a RecurringInvoice",
* description="Handles the deletion of an RecurringInvoice by id",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The RecurringInvoice Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns a HTTP status",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function destroy(DestroyRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice)
{
@ -445,195 +403,30 @@ class RecurringInvoiceController extends BaseController
}
/**
* @OA\Get(
* path="/api/v1/recurring_invoice/{invitation_key}/download",
* operationId="downloadRecurringInvoice",
* tags={"invoices"},
* summary="Download a specific invoice by invitation key",
* description="Downloads a specific invoice",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="invitation_key",
* in="path",
* description="The Recurring Invoice Invitation Key",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the recurring invoice pdf",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
* @param $invitation_key
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadPdf($invitation_key)
public function bulk(BulkRecurringInvoiceRequest $request)
{
$invitation = $this->recurring_invoice_repo->getInvitationByKey($invitation_key);
$contact = $invitation->contact;
$recurring_invoice = $invitation->recurring_invoice;
$percentage_increase = request()->has('percentage_increase') ? request()->input('percentage_increase') : 0;
$file = $recurring_invoice->service()->getInvoicePdf($contact);
if (in_array($request->action, ['increase_prices', 'update_prices'])) {
UpdateRecurring::dispatch($request->ids, auth()->user()->company(), auth()->user(), $request->action, $percentage_increase);
return response()->streamDownload(function () use ($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
}
return response()->json(['message' => 'Update in progress.'], 200);
}
/**
* Perform bulk actions on the list view.
*
* @return Collection
*
*
* @OA\Post(
* path="/api/v1/recurring_invoices/bulk",
* operationId="bulkRecurringInvoices",
* tags={"recurring_invoices"},
* summary="Performs bulk actions on an array of recurring_invoices",
* description="",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/index"),
* @OA\RequestBody(
* description="Hashed IDs",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="array",
* @OA\Items(
* type="integer",
* description="Array of hashed IDs to be bulk 'actioned",
* example="[0,1,2,3]",
* ),
* )
* )
* ),
* @OA\Response(
* response=200,
* description="The RecurringInvoice response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/RecurringInvoice"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
$recurring_invoices = RecurringInvoice::withTrashed()->find($request->ids);
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function bulk()
{
$action = request()->input('action');
$ids = request()->input('ids');
$recurring_invoices = RecurringInvoice::withTrashed()->find($this->transformKeys($ids));
$recurring_invoices->each(function ($recurring_invoice, $key) use ($action) {
$recurring_invoices->each(function ($recurring_invoice, $key) use ($request) {
if (auth()->user()->can('edit', $recurring_invoice)) {
$this->performAction($recurring_invoice, $action, true);
$this->performAction($recurring_invoice, $request->action, true);
}
});
return $this->listResponse(RecurringInvoice::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return $this->listResponse(RecurringInvoice::withTrashed()->whereIn('id', $request->ids));
}
/**
* Recurring Invoice Actions.
*
*
* @OA\Get(
* path="/api/v1/recurring_invoices/{id}/{action}",
* operationId="actionRecurringInvoice",
* tags={"recurring_invoices"},
* summary="Performs a custom action on an RecurringInvoice",
* description="Performs a custom action on an RecurringInvoice.
The current range of actions are as follows
- clone_to_RecurringInvoice
- clone_to_quote
- history
- delivery_note
- mark_paid
- download
- archive
- delete
- email",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The RecurringInvoice Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Parameter(
* name="action",
* in="path",
* description="The action string to be performed",
* example="clone_to_quote",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the RecurringInvoice object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/RecurringInvoice"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
* @param ActionRecurringInvoiceRequest $request
* @param RecurringInvoice $recurring_invoice
* @param $action
@ -763,4 +556,29 @@ class RecurringInvoiceController extends BaseController
return $this->itemResponse($recurring_invoice->fresh());
}
public function downloadPdf(string $invitation_key)
{
$invitation = $this->recurring_invoice_repo->getInvitationByKey($invitation_key);
if (! $invitation) {
return response()->json(['message' => 'no record found'], 400);
}
$contact = $invitation->contact;
$invoice = $invitation->recurring_invoice;
$file = $invoice->service()->getInvoicePdf($contact);
$headers = ['Content-Type' => 'application/pdf'];
if (request()->input('inline') == 'true') {
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
}
return response()->streamDownload(function () use ($file) {
echo Storage::get($file);
}, basename($file), $headers);
}
}

View File

@ -38,6 +38,7 @@ class SubdomainController extends BaseController
'beta',
'prometh',
'license',
'socket',
];
public function __construct()

View File

@ -13,8 +13,8 @@ namespace App\Http\Controllers;
use App\Factory\SchedulerFactory;
use App\Filters\SchedulerFilters;
use App\Http\Requests\TaskScheduler\DestroySchedulerRequest;
use App\Http\Requests\TaskScheduler\CreateSchedulerRequest;
use App\Http\Requests\TaskScheduler\DestroySchedulerRequest;
use App\Http\Requests\TaskScheduler\ShowSchedulerRequest;
use App\Http\Requests\TaskScheduler\StoreSchedulerRequest;
use App\Http\Requests\TaskScheduler\UpdateSchedulerRequest;

View File

@ -592,9 +592,9 @@ class UserController extends BaseController
*/
public function detach(DetachCompanyUserRequest $request, User $user)
{
if ($request->entityIsDeleted($user)) {
return $request->disallowUpdate();
}
// if ($request->entityIsDeleted($user)) {
// return $request->disallowUpdate();
// }
$company_user = CompanyUser::whereUserId($user->id)
->whereCompanyId(auth()->user()->companyId())

View File

@ -17,14 +17,17 @@ use App\Filters\WebhookFilters;
use App\Http\Requests\Webhook\CreateWebhookRequest;
use App\Http\Requests\Webhook\DestroyWebhookRequest;
use App\Http\Requests\Webhook\EditWebhookRequest;
use App\Http\Requests\Webhook\RetryWebhookRequest;
use App\Http\Requests\Webhook\ShowWebhookRequest;
use App\Http\Requests\Webhook\StoreWebhookRequest;
use App\Http\Requests\Webhook\UpdateWebhookRequest;
use App\Jobs\Util\WebhookSingle;
use App\Models\Webhook;
use App\Repositories\BaseRepository;
use App\Transformers\WebhookTransformer;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
class WebhookController extends BaseController
{
@ -487,4 +490,28 @@ class WebhookController extends BaseController
return $this->listResponse(Webhook::withTrashed()->whereIn('id', $this->transformKeys($ids)));
}
public function retry(RetryWebhookRequest $request, Webhook $webhook)
{
match ($request->entity) {
'invoice' => $includes ='client',
'payment' => $includes ='invoices,client',
'project' => $includes ='client',
'purchase_order' => $includes ='vendor',
'quote' => $includes ='client',
default => $includes = ''
};
$class = 'App\Models\\'.ucfirst(Str::camel($request->entity));
$entity = $class::withTrashed()->where('id', $this->decodePrimaryKey($request->entity_id))->company()->first();
if (!$entity) {
return response()->json(['message' => ctrans('texts.record_not_found')], 400);
}
WebhookSingle::dispatchSync($webhook->id, $entity, auth()->user()->company()->db, $includes);
return $this->itemResponse($webhook);
}
}

View File

@ -11,54 +11,55 @@
namespace App\Http;
use App\Http\Middleware\ApiSecretCheck;
use App\Utils\Ninja;
use App\Http\Middleware\Cors;
use App\Http\Middleware\SetDb;
use App\Http\Middleware\Locale;
use App\Http\Middleware\SetWebDb;
use App\Http\Middleware\UrlSetDb;
use App\Http\Middleware\TokenAuth;
use App\Http\Middleware\SetEmailDb;
use App\Http\Middleware\VerifyHash;
use App\Http\Middleware\SetInviteDb;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\CheckClientExistence;
use App\Http\Middleware\CheckForMaintenanceMode;
use App\Http\Middleware\ClientPortalEnabled;
use App\Http\Middleware\ContactSetDb;
use App\Http\Middleware\QueryLogging;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\UserVerified;
use App\Http\Middleware\VendorLocale;
use App\Http\Middleware\PhantomSecret;
use App\Http\Middleware\SetDocumentDb;
use App\Http\Middleware\ApiSecretCheck;
use App\Http\Middleware\ContactAccount;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\SessionDomains;
use App\Http\Middleware\ContactKeyLogin;
use App\Http\Middleware\ContactRegister;
use App\Http\Middleware\ContactSetDb;
use App\Http\Middleware\ContactTokenAuth;
use App\Http\Middleware\Cors;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\Locale;
use App\Http\Middleware\PasswordProtection;
use App\Http\Middleware\PhantomSecret;
use App\Http\Middleware\QueryLogging;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\SessionDomains;
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;
use App\Http\Middleware\SetWebDb;
use App\Http\Middleware\Shop\ShopTokenAuth;
use App\Http\Middleware\TokenAuth;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\UrlSetDb;
use App\Http\Middleware\UserVerified;
use App\Http\Middleware\VendorContactKeyLogin;
use App\Http\Middleware\VendorLocale;
use App\Http\Middleware\VerifyCsrfToken;
use App\Http\Middleware\VerifyHash;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use App\Http\Middleware\ContactTokenAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use App\Http\Middleware\SetDbByCompanyKey;
use App\Http\Middleware\PasswordProtection;
use App\Http\Middleware\ClientPortalEnabled;
use App\Http\Middleware\CheckClientExistence;
use App\Http\Middleware\VendorContactKeyLogin;
use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Routing\Middleware\ValidateSignature;
use Illuminate\Session\Middleware\StartSession;
use App\Http\Middleware\CheckForMaintenanceMode;
use App\Http\Middleware\RedirectIfAuthenticated;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Routing\Middleware\ValidateSignature;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Routing\Middleware\ThrottleRequestsWithRedis;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
class Kernel extends HttpKernel
{
@ -75,9 +76,7 @@ class Kernel extends HttpKernel
TrimStrings::class,
ConvertEmptyStringsToNull::class,
TrustProxies::class,
// \Illuminate\Http\Middleware\HandleCors::class,
Cors::class,
];
/**
@ -140,7 +139,6 @@ class Kernel extends HttpKernel
'cors' => Cors::class,
'guest' => RedirectIfAuthenticated::class,
'signed' => ValidateSignature::class,
'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
'query_logging' => QueryLogging::class,
'token_auth' => TokenAuth::class,
@ -152,7 +150,6 @@ class Kernel extends HttpKernel
'email_db' => SetEmailDb::class,
'invite_db' => SetInviteDb::class,
'password_protected' => PasswordProtection::class,
'signed' => ValidateSignature::class,
'portal_enabled' => ClientPortalEnabled::class,
'url_db' => UrlSetDb::class,
'web_db' => SetWebDb::class,
@ -162,7 +159,6 @@ class Kernel extends HttpKernel
'vendor_locale' => VendorLocale::class,
'contact_register' => ContactRegister::class,
'verify_hash' => VerifyHash::class,
'shop_token_auth' => ShopTokenAuth::class,
'phantom_secret' => PhantomSecret::class,
'contact_key_login' => ContactKeyLogin::class,
'vendor_contact_key_login' => VendorContactKeyLogin::class,
@ -170,6 +166,7 @@ class Kernel extends HttpKernel
'user_verified' => UserVerified::class,
'document_db' => SetDocumentDb::class,
'session_domain' => SessionDomains::class,
//we dyanamically add the throttle middleware in RouteServiceProvider
];
protected $middlewarePriority = [
@ -189,7 +186,6 @@ class Kernel extends HttpKernel
ContactTokenAuth::class,
ContactKeyLogin::class,
Authenticate::class,
ShopTokenAuth::class,
ContactRegister::class,
PhantomSecret::class,
CheckClientExistence::class,
@ -199,4 +195,5 @@ class Kernel extends HttpKernel
SubstituteBindings::class,
ContactAccount::class,
];
}

View File

@ -11,24 +11,24 @@
namespace App\Http\Livewire;
use App\Utils\Ninja;
use App\Models\Client;
use App\Models\Invoice;
use Livewire\Component;
use App\Libraries\MultiDB;
use Illuminate\Support\Str;
use App\Models\Subscription;
use App\Models\ClientContact;
use App\DataMapper\ClientSettings;
use App\Factory\ClientFactory;
use App\Jobs\Mail\NinjaMailerJob;
use App\DataMapper\ClientSettings;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\ContactPasswordlessLogin;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\Subscription;
use App\Repositories\ClientContactRepository;
use App\Repositories\ClientRepository;
use App\Services\Subscription\SubscriptionService;
use App\Utils\Ninja;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use App\Mail\ContactPasswordlessLogin;
use App\Repositories\ClientRepository;
use App\Repositories\ClientContactRepository;
use App\Services\Subscription\SubscriptionService;
use Illuminate\Support\Str;
use Livewire\Component;
class BillingPortalPurchase extends Component
{
@ -401,7 +401,7 @@ class BillingPortalPurchase extends Component
$context = 'purchase';
// if(Ninja::isHosted() && $this->subscription->service()->recurring_products()->first()?->id == SubscriptionService::WHITE_LABEL) {
if(Ninja::isHosted() && $this->subscription->service()->recurring_products()->first()?->product_key == 'whitelabel') {
if (Ninja::isHosted() && $this->subscription->service()->recurring_products()->first()?->product_key == 'whitelabel') {
$context = 'whitelabel';
}

View File

@ -172,8 +172,7 @@ class BillingPortalPurchasev2 extends Component
$this->contact = auth()->guard('contact')->user();
$this->authenticated = true;
$this->payment_started = true;
}
else {
} else {
$this->bundle = collect();
}
@ -276,7 +275,6 @@ class BillingPortalPurchasev2 extends Component
*/
public function handleCoupon()
{
$this->resetErrorBag('coupon');
$this->resetValidation('coupon');
@ -311,7 +309,7 @@ class BillingPortalPurchasev2 extends Component
'description' => $p->notes,
'product_key' => $p->product_key,
'unit_cost' => $p->price,
'product' => nl2br(substr($p->notes, 0, 50)),
'product' => substr(strip_tags($p->markdownNotes()), 0, 50),
'price' => Number::formatMoney($total, $this->subscription->company).' / '. RecurringInvoice::frequencyForKey($this->subscription->frequency_id),
'total' => $total,
'qty' => $qty,
@ -329,7 +327,7 @@ class BillingPortalPurchasev2 extends Component
'description' => $p->notes,
'product_key' => $p->product_key,
'unit_cost' => $p->price,
'product' => nl2br(substr($p->notes, 0, 50)),
'product' => substr(strip_tags($p->markdownNotes()), 0, 50),
'price' => Number::formatMoney($total, $this->subscription->company),
'total' => $total,
'qty' => $qty,
@ -352,7 +350,7 @@ class BillingPortalPurchasev2 extends Component
'description' => $p->notes,
'product_key' => $p->product_key,
'unit_cost' => $p->price,
'product' => nl2br(substr($p->notes, 0, 50)),
'product' => substr(strip_tags($p->markdownNotes()), 0, 50),
'price' => Number::formatMoney($total, $this->subscription->company).' / '. RecurringInvoice::frequencyForKey($this->subscription->frequency_id),
'total' => $total,
'qty' => $qty,
@ -375,7 +373,7 @@ class BillingPortalPurchasev2 extends Component
'description' => $p->notes,
'product_key' => $p->product_key,
'unit_cost' => $p->price,
'product' => nl2br(substr($p->notes, 0, 50)),
'product' => substr(strip_tags($p->markdownNotes()), 0, 50),
'price' => Number::formatMoney($total, $this->subscription->company),
'total' => $total,
'qty' => $qty,

View File

@ -232,8 +232,9 @@ class RequiredClientInfo extends Component
if ($cg && $cg->update_details) {
$payment_gateway = $cg->driver($this->client)->init();
// if(method_exists($payment_gateway, "updateCustomer"))
// $payment_gateway->updateCustomer();
if (method_exists($payment_gateway, "updateCustomer")) {
$payment_gateway->updateCustomer();
}
}
return true;

View File

@ -0,0 +1,124 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Redis\Limiters\DurationLimiter;
use Illuminate\Routing\Middleware\ThrottleRequests;
class ThrottleRequestsWithPredis extends ThrottleRequests
{
/**
* The Redis factory implementation.
*
* @var \Illuminate\Contracts\Redis\Factory
*/
protected $redis;
/**
* The timestamp of the end of the current duration by key.
*
* @var array
*/
public $decaysAt = [];
/**
* The number of remaining slots by key.
*
* @var array
*/
public $remaining = [];
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @return void
*/
public function __construct(RateLimiter $limiter)
{
parent::__construct($limiter);
$this->redis = \Illuminate\Support\Facades\Redis::connection('sentinel-cache');
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param array $limits
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
}
$response = $next($request);
foreach ($limits as $limit) {
$response = $this->addHeaders(
$response,
$limit->maxAttempts,
$this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
);
}
return $response;
}
/**
* Determine if the given key has been "accessed" too many times.
*
* @param string $key
* @param int $maxAttempts
* @param int $decayMinutes
* @return mixed
*/
protected function tooManyAttempts($key, $maxAttempts, $decayMinutes)
{
$limiter = new DurationLimiter(
$this->redis,
$key,
$maxAttempts,
$decayMinutes * 60
);
return tap(! $limiter->acquire(), function () use ($key, $limiter) {
[$this->decaysAt[$key], $this->remaining[$key]] = [
$limiter->decaysAt, $limiter->remaining,
];
});
}
/**
* Calculate the number of remaining attempts.
*
* @param string $key
* @param int $maxAttempts
* @param int|null $retryAfter
* @return int
*/
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
return is_null($retryAfter) ? $this->remaining[$key] : 0;
}
/**
* Get the number of seconds until the lock is released.
*
* @param string $key
* @return int
*/
protected function getTimeUntilNextRetry($key)
{
return $this->decaysAt[$key] - $this->currentTime();
}
}

View File

@ -29,10 +29,11 @@ class UploadBankIntegrationRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -29,10 +29,11 @@ class UploadBankTransactionRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -38,10 +38,11 @@ class StoreClientRequest extends Request
public function rules()
{
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -38,10 +38,11 @@ class UpdateClientRequest extends Request
{
/* Ensure we have a client name, and that all emails are unique*/
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -29,10 +29,11 @@ class UploadClientRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests\ClientPortal\PrePayments;
use App\Http\ViewComposers\PortalComposer;
use Illuminate\Foundation\Http\FormRequest;
class StorePrePaymentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->guard('contact')->user()->company->enabled_modules & PortalComposer::MODULE_INVOICES;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'notes' => 'required|bail|',
'amount' => 'required|bail|gte:minimum_amount|numeric',
'minimum_amount' => '',
];
}
public function prepareForValidation()
{
$input = $this->all();
$this->replace($input);
}
}

View File

@ -29,10 +29,11 @@ class UploadCompanyRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -43,10 +43,11 @@ class StoreCreditRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -42,10 +42,11 @@ class UpdateCreditRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -29,10 +29,11 @@ class UploadCreditRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -32,13 +32,18 @@ class StoreDesignRequest extends Request
return [
//'name' => 'required',
'name' => 'required|unique:designs,name,null,null,company_id,'.auth()->user()->companyId(),
'design' => 'required',
'design' => 'required|array',
'design.header' => 'required|min:1',
'design.body' => 'required|min:1',
'design.footer' => 'required|min:1',
'design.includes' => 'required|min:1',
];
}
public function prepareForValidation()
{
$input = $this->all();
$input['design'] = (isset($input['design']) && is_array($input['design'])) ? $input['design'] : [];
if (! array_key_exists('product', $input['design']) || is_null($input['design']['product'])) {
$input['design']['product'] = '';

View File

@ -43,6 +43,7 @@ class SendEmailRequest extends Request
'template' => 'bail|required',
'entity' => 'bail|required',
'entity_id' => 'bail|required',
'cc_email' => 'bail|sometimes|email|nullable',
];
}

View File

@ -29,10 +29,11 @@ class UploadExpenseRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -29,10 +29,11 @@ class UploadGroupSettingRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -37,10 +37,11 @@ class StoreInvoiceRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -39,10 +39,11 @@ class UpdateInvoiceRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
use Illuminate\Http\UploadedFile;
class UploadInvoiceRequest extends Request
{
@ -29,10 +30,11 @@ class UploadInvoiceRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;
@ -46,5 +48,23 @@ class UploadInvoiceRequest extends Request
public function prepareForValidation()
{
//tests to see if upload via binary data works.
// if(request()->getContent())
// {
// // $file = new UploadedFile(request()->getContent(), request()->header('filename'));
// $file = new UploadedFile(request()->getContent(), 'something.png');
// // request()->files->set('documents', $file);
// $this->files->add(['file' => $file]);
// // Merge it in request also (As I found this is not needed in every case)
// $this->merge(['file' => $file]);
// }
}
}

View File

@ -112,10 +112,11 @@ class StorePaymentRequest extends Request
];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -44,10 +44,11 @@ class UpdatePaymentRequest extends Request
$rules['number'] = Rule::unique('payments')->where('company_id', auth()->user()->company()->id)->ignore($this->payment->id);
}
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -29,10 +29,11 @@ class UploadPaymentRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -47,6 +47,7 @@ class DesignPreviewRequest extends Request
'settings' => 'sometimes',
'group_id' => 'sometimes',
'client_id' => 'sometimes',
'design' => 'bail|sometimes|array'
];
return $rules;

View File

@ -12,10 +12,6 @@
namespace App\Http\Requests\Preview;
use App\Http\Requests\Request;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;

View File

@ -12,7 +12,6 @@
namespace App\Http\Requests\Preview;
use App\Http\Requests\Request;
use App\Models\PurchaseOrder;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;

View File

@ -28,10 +28,11 @@ class StoreProductRequest extends Request
public function rules()
{
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -12,7 +12,6 @@
namespace App\Http\Requests\Product;
use App\Http\Requests\Request;
use App\Models\Product;
use App\Utils\Traits\ChecksEntityStatus;
class UpdateProductRequest extends Request
@ -31,10 +30,11 @@ class UpdateProductRequest extends Request
public function rules()
{
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -28,10 +28,11 @@ class UploadProductRequest extends Request
public function rules()
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -11,8 +11,8 @@
namespace App\Http\Requests\Project;
use App\Models\Project;
use App\Http\Requests\Request;
use App\Models\Project;
class CreateProjectRequest extends Request
{
@ -24,6 +24,5 @@ class CreateProjectRequest extends Request
public function authorize() : bool
{
return auth()->user()->can('create', Project::class);
}
}

View File

@ -24,7 +24,6 @@ class ShowProjectRequest extends Request
{
// return auth()->user()->isAdmin();
return auth()->user()->can('view', $this->project);
}
/**

View File

@ -42,10 +42,11 @@ class StoreProjectRequest extends Request
$rules['number'] = Rule::unique('projects')->where('company_id', auth()->user()->company()->id);
}
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -37,10 +37,11 @@ class UpdateProjectRequest extends Request
$rules['number'] = Rule::unique('projects')->where('company_id', auth()->user()->company()->id)->ignore($this->project->id);
}
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -29,10 +29,11 @@ class UploadProjectRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -47,10 +47,11 @@ class StorePurchaseOrderRequest extends Request
$rules['is_amount_discount'] = ['boolean'];
$rules['line_items'] = 'array';
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -50,10 +50,11 @@ class UpdatePurchaseOrderRequest extends Request
$rules['discount'] = 'sometimes|numeric';
$rules['is_amount_discount'] = ['boolean'];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

View File

@ -29,10 +29,11 @@ class UploadPurchaseOrderRequest extends Request
{
$rules = [];
if($this->file('documents') && is_array($this->file('documents')))
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation;
elseif($this->file('documents'))
} elseif ($this->file('documents')) {
$rules['documents'] = $this->file_validation;
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->file_validation;

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