1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-28 04:07:10 +02:00

Merge pull request #6186 from turbo124/v5-stable

v5.2.8
This commit is contained in:
David Bomba 2021-07-02 17:29:35 +10:00 committed by GitHub
commit f74fd9f304
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 222988 additions and 219625 deletions

View File

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

View File

@ -16,19 +16,21 @@ use App\Factory\ClientContactFactory;
use App\Models\Account; use App\Models\Account;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyLedger; use App\Models\CompanyLedger;
use App\Models\Contact; use App\Models\Contact;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Paymentable;
use App\Utils\Ninja; use App\Utils\Ninja;
use DB; use DB;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Mail; use Mail;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Illuminate\Support\Str;
/* /*
@ -84,6 +86,8 @@ class CheckData extends Command
public function handle() public function handle()
{ {
$time_start = microtime(true);
$database_connection = $this->option('database') ? $this->option('database') : 'Connected to Default DB'; $database_connection = $this->option('database') ? $this->option('database') : 'Connected to Default DB';
$fix_status = $this->option('fix') ? "Fixing Issues" : "Just checking issues "; $fix_status = $this->option('fix') ? "Fixing Issues" : "Just checking issues ";
@ -96,6 +100,7 @@ class CheckData extends Command
$this->checkInvoiceBalances(); $this->checkInvoiceBalances();
$this->checkInvoicePayments(); $this->checkInvoicePayments();
$this->checkPaidToDates(); $this->checkPaidToDates();
// $this->checkPaidToCompanyDates();
$this->checkClientBalances(); $this->checkClientBalances();
$this->checkContacts(); $this->checkContacts();
$this->checkCompanyData(); $this->checkCompanyData();
@ -107,6 +112,8 @@ class CheckData extends Command
} }
$this->logMessage('Done: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE)); $this->logMessage('Done: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE));
$this->logMessage('Total execution time in seconds: ' . (microtime(true) - $time_start));
$errorEmail = config('ninja.error_email'); $errorEmail = config('ninja.error_email');
if ($errorEmail) { if ($errorEmail) {
@ -307,32 +314,93 @@ class CheckData extends Command
} }
} }
// private function checkPaidToCompanyDates()
// {
// Company::cursor()->each(function ($company){
// $payments = Payment::where('is_deleted', 0)
// ->where('company_id', $company->id)
// ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED])
// ->pluck('id');
// $unapplied = Payment::where('is_deleted', 0)
// ->where('company_id', $company->id)
// ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
// ->sum(\DB::Raw('amount - applied'));
// $paymentables = Paymentable::whereIn('payment_id', $payments)->sum(\DB::Raw('amount - refunded'));
// $client_paid_to_date = Client::where('company_id', $company->id)->where('is_deleted', 0)->withTrashed()->sum('paid_to_date');
// $total_payments = $paymentables + $unapplied;
// if (round($total_payments, 2) != round($client_paid_to_date, 2)) {
// $this->wrong_paid_to_dates++;
// $this->logMessage($company->present()->name.' id = # '.$company->id." - Paid to date does not match Client Paid To Date = {$client_paid_to_date} - Invoice Payments = {$total_payments}");
// }
// });
// }
private function checkPaidToDates() private function checkPaidToDates()
{ {
$this->wrong_paid_to_dates = 0; $this->wrong_paid_to_dates = 0;
$credit_total_applied = 0; $credit_total_applied = 0;
Client::withTrashed()->where('is_deleted', 0)->cursor()->each(function ($client) use ($credit_total_applied) {
$clients = DB::table('clients')
->leftJoin('payments', function($join) {
$join->on('payments.client_id', '=', 'clients.id')
->where('payments.is_deleted', 0)
->whereIn('payments.status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED]);
})
->where('clients.is_deleted',0)
->where('clients.updated_at', '>', now()->subDays(2))
->groupBy('clients.id')
->havingRaw('clients.paid_to_date != sum(coalesce(payments.amount - payments.refunded, 0))')
->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(coalesce(payments.amount - payments.refunded, 0)) as amount')]);
/* Due to accounting differences we need to perform a second loop here to ensure there actually is an issue */
$clients->each(function ($client_record) use ($credit_total_applied) {
$client = Client::find($client_record->id);
$total_invoice_payments = 0; $total_invoice_payments = 0;
foreach ($client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get() as $invoice) { foreach ($client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get() as $invoice) {
$total_amount = $invoice->payments()->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->get()->sum('pivot.amount'); $total_invoice_payments += $invoice->payments()
$total_refund = $invoice->payments()->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->get()->sum('pivot.refunded'); ->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->selectRaw('sum(paymentables.amount - paymentables.refunded) as p')
->pluck('p')
->first();
$total_invoice_payments += ($total_amount - $total_refund);
} }
//commented IN 27/06/2021 - sums ALL client payments AND the unapplied amounts to match the client paid to date
$p = Payment::where('client_id', $client->id)
->where('is_deleted', 0)
->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->sum(DB::Raw('amount - applied'));
$total_invoice_payments += $p;
// 10/02/21 // 10/02/21
foreach ($client->payments as $payment) { foreach ($client->payments as $payment) {
$credit_total_applied += $payment->paymentables()->where('paymentable_type', App\Models\Credit::class)->get()->sum(DB::raw('amount'));
$credit_total_applied += $payment->paymentables()
->where('paymentable_type', App\Models\Credit::class)
->selectRaw('sum(paymentables.amount - paymentables.refunded) as p')
->pluck('p')
->first();
} }
if ($credit_total_applied < 0) { if ($credit_total_applied < 0) {
$total_invoice_payments += $credit_total_applied; $total_invoice_payments += $credit_total_applied;
} }
if (round($total_invoice_payments, 2) != round($client->paid_to_date, 2)) { if (round($total_invoice_payments, 2) != round($client->paid_to_date, 2)) {
$this->wrong_paid_to_dates++; $this->wrong_paid_to_dates++;
@ -355,20 +423,28 @@ class CheckData extends Command
{ {
$this->wrong_balances = 0; $this->wrong_balances = 0;
Client::cursor()->where('is_deleted', 0)->each(function ($client) { Client::cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->each(function ($client) {
$client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($client) { $client->invoices->where('is_deleted', false)->whereIn('status_id', '!=', Invoice::STATUS_DRAFT)->each(function ($invoice) use ($client) {
$total_amount = $invoice->payments()->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED])->get()->sum('pivot.amount'); // $total_amount = $invoice->payments()->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED])->get()->sum('pivot.amount');
$total_refund = $invoice->payments()->get()->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED])->sum('pivot.refunded'); // $total_refund = $invoice->payments()->get()->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED])->sum('pivot.refunded');
$total_paid = $invoice->payments()
->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->selectRaw('sum(paymentables.amount - paymentables.refunded) as p')
->pluck('p')
->first();
// $total_paid = $total_amount - $total_refund;
$total_credit = $invoice->credits()->get()->sum('amount'); $total_credit = $invoice->credits()->get()->sum('amount');
$total_paid = $total_amount - $total_refund;
$calculated_paid_amount = $invoice->amount - $invoice->balance - $total_credit; $calculated_paid_amount = $invoice->amount - $invoice->balance - $total_credit;
if ((string)$total_paid != (string)($invoice->amount - $invoice->balance - $total_credit)) { if ((string)$total_paid != (string)($invoice->amount - $invoice->balance - $total_credit)) {
$this->wrong_balances++; $this->wrong_balances++;
$this->logMessage($client->present()->name.' - '.$client->id." - Total Amount = {$total_amount} != Calculated Total = {$calculated_paid_amount} - Total Refund = {$total_refund} Total credit = {$total_credit}"); $this->logMessage($client->present()->name.' - '.$client->id." - Total Paid = {$total_paid} != Calculated Total = {$calculated_paid_amount}");
$this->isValid = false; $this->isValid = false;
} }
@ -384,7 +460,7 @@ class CheckData extends Command
$this->wrong_balances = 0; $this->wrong_balances = 0;
$this->wrong_paid_to_dates = 0; $this->wrong_paid_to_dates = 0;
foreach (Client::cursor()->where('is_deleted', 0) as $client) { foreach (Client::cursor()->where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2)) as $client) {
//$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance'); //$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$invoice_balance = Invoice::where('client_id', $client->id)->where('is_deleted', false)->where('status_id', '>', 1)->withTrashed()->sum('balance'); $invoice_balance = Invoice::where('client_id', $client->id)->where('is_deleted', false)->where('status_id', '>', 1)->withTrashed()->sum('balance');
$credit_balance = Credit::where('client_id', $client->id)->where('is_deleted', false)->withTrashed()->sum('balance'); $credit_balance = Credit::where('client_id', $client->id)->where('is_deleted', false)->withTrashed()->sum('balance');
@ -418,12 +494,9 @@ class CheckData extends Command
$this->wrong_balances = 0; $this->wrong_balances = 0;
$this->wrong_paid_to_dates = 0; $this->wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->cursor() as $client) { foreach (Client::where('is_deleted', 0)->where('clients.updated_at', '>', now()->subDays(2))->cursor() as $client) {
$invoice_balance = $client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get()->sum('balance'); $invoice_balance = $client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$credit_balance = $client->credits()->where('is_deleted', false)->get()->sum('balance'); $credit_balance = $client->credits()->where('is_deleted', false)->sum('balance');
// if($client->balance != $invoice_balance)
// $invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();

View File

@ -35,8 +35,9 @@ class ContactLoginController extends Controller
public function showLoginForm(Request $request) public function showLoginForm(Request $request)
{ {
if ($request->subdomain) { if (strpos($request->getHost(), 'invoicing.co') !== false) {
$company = Company::where('subdomain', $request->subdomain)->first(); $subdomain = explode('.', $request->getHost())[0];
$company = Company::where('subdomain', $subdomain)->first();
} elseif (Ninja::isSelfHost()) { } elseif (Ninja::isSelfHost()) {
$company = Account::first()->default_company; $company = Account::first()->default_company;
} else { } else {

View File

@ -319,7 +319,8 @@ class PaymentController extends Controller
* @return Response The response view * @return Response The response view
*/ */
public function credit_response(Request $request) public function credit_response(Request $request)
{ {
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->first(); $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->first();
/* Hydrate the $payment */ /* Hydrate the $payment */

View File

@ -14,7 +14,7 @@ namespace App\Http\Controllers\ClientPortal;
use App\Events\Payment\Methods\MethodDeleted; use App\Events\Payment\Methods\MethodDeleted;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\CreatePaymentMethodRequest; use App\Http\Requests\ClientPortal\PaymentMethod\CreatePaymentMethodRequest;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
@ -52,7 +52,7 @@ class PaymentMethodController extends Controller
$data['gateway'] = $gateway; $data['gateway'] = $gateway;
$data['client'] = auth()->user()->client; $data['client'] = auth()->user()->client;
return $gateway return $gateway
->driver(auth()->user()->client) ->driver(auth()->user()->client)
->setPaymentMethod($request->query('method')) ->setPaymentMethod($request->query('method'))
@ -93,7 +93,7 @@ class PaymentMethodController extends Controller
public function verify(ClientGatewayToken $payment_method) public function verify(ClientGatewayToken $payment_method)
{ {
// $gateway = $this->getClientGateway(); // $gateway = $this->getClientGateway();
return $payment_method->gateway return $payment_method->gateway
->driver(auth()->user()->client) ->driver(auth()->user()->client)
->setPaymentMethod(request()->query('method')) ->setPaymentMethod(request()->query('method'))

View File

@ -476,8 +476,10 @@ class CompanyController extends BaseController
{ {
$company_count = $company->account->companies->count(); $company_count = $company->account->companies->count();
$account = $company->account; $account = $company->account;
$account_key = $account->key;
if ($company_count == 1) { if ($company_count == 1) {
$company->company_users->each(function ($company_user) { $company->company_users->each(function ($company_user) {
$company_user->user->forceDelete(); $company_user->user->forceDelete();
$company_user->forceDelete(); $company_user->forceDelete();
@ -485,9 +487,13 @@ class CompanyController extends BaseController
$account->delete(); $account->delete();
if(Ninja::isHosted())
\Modules\Admin\Jobs\Account\NinjaDeletedAccount::dispatch($account_key);
LightLogs::create(new AccountDeleted()) LightLogs::create(new AccountDeleted())
->increment() ->increment()
->batch(); ->batch();
} else { } else {
$company_id = $company->id; $company_id = $company->id;

View File

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

View File

@ -387,7 +387,6 @@ class MigrationController extends BaseController
else else
StartMigration::dispatch($migration_file, $user, $fresh_company); StartMigration::dispatch($migration_file, $user, $fresh_company);
} }
} }

View File

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

View File

@ -109,11 +109,11 @@ class SetupController extends Controller
'REQUIRE_HTTPS' => $request->input('https') ? 'true' : 'false', 'REQUIRE_HTTPS' => $request->input('https') ? 'true' : 'false',
'APP_DEBUG' => 'false', 'APP_DEBUG' => 'false',
'DB_HOST1' => $request->input('db_host'), 'DB_HOST' => $request->input('db_host'),
'DB_PORT1' => $request->input('db_port'), 'DB_PORT' => $request->input('db_port'),
'DB_DATABASE1' => $request->input('db_database'), 'DB_DATABASE' => $request->input('db_database'),
'DB_USERNAME1' => $request->input('db_username'), 'DB_USERNAME' => $request->input('db_username'),
'DB_PASSWORD1' => $request->input('db_password'), 'DB_PASSWORD' => $request->input('db_password'),
'MAIL_MAILER' => $mail_driver, 'MAIL_MAILER' => $mail_driver,
'MAIL_PORT' => $request->input('mail_port'), 'MAIL_PORT' => $request->input('mail_port'),
@ -125,6 +125,7 @@ class SetupController extends Controller
'MAIL_PASSWORD' => $request->input('mail_password'), 'MAIL_PASSWORD' => $request->input('mail_password'),
'NINJA_ENVIRONMENT' => 'selfhost', 'NINJA_ENVIRONMENT' => 'selfhost',
'DB_CONNECTION' => 'mysql',
]; ];
if (config('ninja.db.multi_db_enabled')) { if (config('ninja.db.multi_db_enabled')) {
@ -133,11 +134,11 @@ class SetupController extends Controller
if (config('ninja.preconfigured_install')) { if (config('ninja.preconfigured_install')) {
// Database connection was already configured. Don't let the user override it. // Database connection was already configured. Don't let the user override it.
unset($env_values['DB_HOST1']); unset($env_values['DB_HOST']);
unset($env_values['DB_PORT1']); unset($env_values['DB_PORT']);
unset($env_values['DB_DATABASE1']); unset($env_values['DB_DATABASE']);
unset($env_values['DB_USERNAME1']); unset($env_values['DB_USERNAME']);
unset($env_values['DB_PASSWORD1']); unset($env_values['DB_PASSWORD']);
} }
try { try {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests\ClientPortal\PaymentMethod;
use App\Http\Requests\Request;
use App\Models\Client;
use Illuminate\Foundation\Http\FormRequest;
use function auth;
use function collect;
class CreatePaymentMethodRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
/** @var Client $client */
$client = auth()->user()->client;
$available_methods = [];
collect($client->service()->getPaymentMethods(1))
->filter(function ($method) use (&$available_methods) {
$available_methods[] = $method['gateway_type_id'];
});
if (in_array($this->query('method'), $available_methods)) {
return true;
}
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\ClientPortal; namespace App\Http\Requests\ClientPortal;
use App\Libraries\MultiDB;
use App\Models\Account; use App\Models\Account;
use App\Models\Company; use App\Models\Company;
use App\Utils\Ninja; use App\Utils\Ninja;
@ -26,20 +27,27 @@ class RegisterRequest extends FormRequest
*/ */
public function rules() public function rules()
{ {
return [ $rules = [
'first_name' => ['required', 'string', 'max:255'], 'first_name' => ['required', 'string', 'max:255'],
'last_name' => ['required', 'string', 'max:255'], 'last_name' => ['required', 'string', 'max:255'],
'phone' => ['required', 'string', 'max:255'], 'phone' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email:rfc,dns', 'max:255'], 'email' => ['required', 'string', 'email:rfc,dns', 'max:255'],
'password' => ['required', 'string', 'min:6', 'confirmed'], 'password' => ['required', 'string', 'min:6', 'confirmed'],
]; ];
if ($this->company()->settings->client_portal_terms || $this->company()->settings->client_portal_privacy_policy) {
$rules['terms'] = ['required'];
}
return $rules;
} }
public function company() public function company()
{ {
if ($this->subdomain) {
return Company::where('subdomain', $this->subdomain)->firstOrFail(); //this should be all we need, the rest SHOULD be redundant because of our Middleware
} if ($this->key)
return Company::where('company_key', $this->key)->first();
if ($this->company_key) { if ($this->company_key) {
return Company::where('company_key', $this->company_key)->firstOrFail(); return Company::where('company_key', $this->company_key)->firstOrFail();
@ -48,11 +56,34 @@ class RegisterRequest extends FormRequest
if (!$this->route()->parameter('company_key') && Ninja::isSelfHost()) { if (!$this->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Account::first()->default_company; $company = Account::first()->default_company;
abort_unless($company->client_can_register, 404); if(!$company->client_can_register)
abort(403, "This page is restricted");
return $company; return $company;
} }
abort(404, 'Register request not found.'); if (Ninja::isHosted()) {
$subdomain = explode('.', $this->getHost())[0];
$query = [
'subdomain' => $subdomain,
'portal_mode' => 'subdomain',
];
if($company = MultiDB::findAndSetDbByDomain($query))
return $company;
$query = [
'portal_domain' => $this->getSchemeAndHttpHost(),
'portal_mode' => 'domain',
];
if($company = MultiDB::findAndSetDbByDomain($query))
return $company;
}
abort(400, 'Register request not found.');
} }
} }

View File

@ -77,6 +77,9 @@ class CreateAccount
$sp794f3f->save(); $sp794f3f->save();
if(Ninja::isHosted())
$sp794f3f->startTrial('pro');
$sp035a66 = CreateCompany::dispatchNow($this->request, $sp794f3f); $sp035a66 = CreateCompany::dispatchNow($this->request, $sp794f3f);
$sp035a66->load('account'); $sp035a66->load('account');
$sp794f3f->default_company_id = $sp035a66->id; $sp794f3f->default_company_id = $sp035a66->id;

View File

@ -20,6 +20,7 @@ use App\Libraries\MultiDB;
use App\Mail\DownloadBackup; use App\Mail\DownloadBackup;
use App\Mail\DownloadInvoices; use App\Mail\DownloadInvoices;
use App\Mail\Import\CompanyImportFailure; use App\Mail\Import\CompanyImportFailure;
use App\Mail\Import\ImportCompleted;
use App\Models\Activity; use App\Models\Activity;
use App\Models\Backup; use App\Models\Backup;
use App\Models\Client; use App\Models\Client;
@ -56,6 +57,7 @@ use App\Models\Vendor;
use App\Models\VendorContact; use App\Models\VendorContact;
use App\Models\Webhook; use App\Models\Webhook;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\TempFile;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -73,6 +75,10 @@ class CompanyImport implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
public $tries = 1;
public $timeout = 0;
protected $current_app_version; protected $current_app_version;
private $account; private $account;
@ -81,7 +87,7 @@ class CompanyImport implements ShouldQueue
public $user; public $user;
private $hash; private $file_location;
public $backup_file; public $backup_file;
@ -142,11 +148,11 @@ class CompanyImport implements ShouldQueue
* @param string $hash - the cache hash of the import data. * @param string $hash - the cache hash of the import data.
* @param array $request->all() * @param array $request->all()
*/ */
public function __construct(Company $company, User $user, string $hash, array $request_array) public function __construct(Company $company, User $user, string $file_location, array $request_array)
{ {
$this->company = $company; $this->company = $company;
$this->user = $user; $this->user = $user;
$this->hash = $hash; $this->file_location = $file_location;
$this->request_array = $request_array; $this->request_array = $request_array;
$this->current_app_version = config('ninja.app_version'); $this->current_app_version = config('ninja.app_version');
} }
@ -160,14 +166,17 @@ class CompanyImport implements ShouldQueue
$this->company_owner = $this->company->owner(); $this->company_owner = $this->company->owner();
nlog("Company ID = {$this->company->id}"); nlog("Company ID = {$this->company->id}");
nlog("Hash ID = {$this->hash}"); nlog("file_location ID = {$this->file_location}");
$this->backup_file = Cache::get($this->hash); // $this->backup_file = Cache::get($this->hash);
if ( empty( $this->backup_file ) ) if ( empty( $this->file_location ) )
throw new \Exception('No import data found, has the cache expired?'); throw new \Exception('No import data found, has the cache expired?');
$this->backup_file = json_decode(base64_decode($this->backup_file)); // $this->backup_file = json_decode(file_get_contents($this->file_location));
$tmp_file = $this->unzipFile();
$this->backup_file = json_decode(file_get_contents($tmp_file));
// nlog($this->backup_file); // nlog($this->backup_file);
$this->checkUserCount(); $this->checkUserCount();
@ -185,6 +194,17 @@ class CompanyImport implements ShouldQueue
->purgeCompanyData() ->purgeCompanyData()
->importData(); ->importData();
$data = [
'errors' => []
];
$nmo = new NinjaMailerObject;
$nmo->mailable = new ImportCompleted($this->company, $data);
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
$nmo->to_user = $this->company->owner();
NinjaMailerJob::dispatchNow($nmo);
} }
catch(\Exception $e){ catch(\Exception $e){
@ -194,8 +214,35 @@ class CompanyImport implements ShouldQueue
} }
unlink($tmp_file);
} }
private function unzipFile()
{
if(mime_content_type(Storage::path($this->file_location)) == 'text/plain')
return Storage::path($this->file_location);
$path = TempFile::filePath(Storage::get($this->file_location), basename($this->file_location));
$zip = new ZipArchive();
$archive = $zip->open($path);
$file_path = sys_get_temp_dir().'/'.sha1(microtime());
$zip->extractTo($file_path);
$zip->close();
$file_location = "{$file_path}/backup.json";
if (! file_exists($file_location))
throw new NonExistingMigrationFile('Backup file does not exist, or is corrupted.');
return $file_location;
}
/** /**
* On the hosted platform we cannot allow the * On the hosted platform we cannot allow the
* import to start if there are users > plan number * import to start if there are users > plan number
@ -293,14 +340,9 @@ class CompanyImport implements ShouldQueue
if($this->pre_flight_checks_pass === false) if($this->pre_flight_checks_pass === false)
{ {
$nmo = new NinjaMailerObject;
$nmo->mailable = new CompanyImportFailure($this->company, $this->message);
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
$nmo->to_user = $this->company->owner();
NinjaMailerJob::dispatchNow($nmo);
nlog($this->message); $this->sendImportMail($this->message);
throw new \Exception($this->message); throw new \Exception($this->message);
} }
@ -362,7 +404,7 @@ class CompanyImport implements ShouldQueue
} }
nlog("finished importing company data"); nlog("finished importing company data");
return $this; return $this;
@ -1116,6 +1158,10 @@ class CompanyImport implements ShouldQueue
foreach($this->backup_file->{$object_property} as $obj) foreach($this->backup_file->{$object_property} as $obj)
{ {
if(is_null($obj))
continue;
/* Remove unwanted keys*/ /* Remove unwanted keys*/
$obj_array = (array)$obj; $obj_array = (array)$obj;
foreach($unset as $un){ foreach($unset as $un){
@ -1236,12 +1282,22 @@ class CompanyImport implements ShouldQueue
if (! array_key_exists($resource, $this->ids)) { if (! array_key_exists($resource, $this->ids)) {
// nlog($this->ids); // nlog($this->ids);
$this->sendImportMail("The Import failed due to missing data in the import file. Resource {$resource} not available.");
throw new \Exception("Resource {$resource} not available."); throw new \Exception("Resource {$resource} not available.");
} }
if (! array_key_exists("{$old}", $this->ids[$resource])) { if (! array_key_exists("{$old}", $this->ids[$resource])) {
// nlog($this->ids[$resource]); // nlog($this->ids[$resource]);
nlog("searching for {$old} in {$resource}"); nlog("searching for {$old} in {$resource}");
nlog("If we are missing a user - default to the company owner");
if($resource == 'users')
return $this->company_owner->id;
$this->sendImportMail("The Import failed due to missing data in the import file. Resource {$resource} not available.");
throw new \Exception("Missing {$resource} key: {$old}"); throw new \Exception("Missing {$resource} key: {$old}");
} }
@ -1249,4 +1305,15 @@ class CompanyImport implements ShouldQueue
} }
private function sendImportMail($message){
$nmo = new NinjaMailerObject;
$nmo->mailable = new CompanyImportFailure($this->company, $message);
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
$nmo->to_user = $this->company->owner();
NinjaMailerJob::dispatchNow($nmo);
}
} }

View File

@ -27,6 +27,7 @@ use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use ZipStream\Option\Archive; use ZipStream\Option\Archive;
use ZipStream\ZipStream; use ZipStream\ZipStream;
use ZipArchive;
class ZipInvoices implements ShouldQueue class ZipInvoices implements ShouldQueue
{ {
@ -62,48 +63,49 @@ class ZipInvoices implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
* *
*
* @return void * @return void
* @throws \ZipStream\Exception\FileNotFoundException * @throws \ZipStream\Exception\FileNotFoundException
* @throws \ZipStream\Exception\FileNotReadableException * @throws \ZipStream\Exception\FileNotReadableException
* @throws \ZipStream\Exception\OverflowException * @throws \ZipStream\Exception\OverflowException
*/ */
public function handle() public function handle()
{ {
$tempStream = fopen('php://memory', 'w+'); # create new zip object
$zip = new ZipArchive();
$options = new Archive(); $invitation = $this->invoices->first()->invitations->first();
$options->setOutputStream($tempStream); $path = $this->invoices->first()->client->invoice_filepath($invitation);
// create a new zipstream object
$file_name = date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip'; $file_name = date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip';
$tmp_file = @tempnam('.', '');
$zip->open($tmp_file , ZipArchive::OVERWRITE);
$invoice = $this->invoices->first(); # loop through each file
$invitation = $invoice->invitations->first();
$path = $invoice->client->invoice_filepath($invitation);
$zip = new ZipStream($file_name, $options);
foreach ($this->invoices as $invoice) { foreach ($this->invoices as $invoice) {
//$zip->addFileFromPath(basename($invoice->pdf_file_path()), TempFile::path($invoice->pdf_file_path()));
$zip->addFileFromPath(basename($invoice->pdf_file_path($invitation)), $invoice->pdf_file_path()); $inv = $invoice->invitations->first();
# download file
$download_file = file_get_contents($invoice->pdf_file_path($inv, 'url', true));
#add it to the zip
$zip->addFromString(basename($invoice->pdf_file_path($inv)), $download_file);
} }
$zip->finish(); # close zip
$zip->close();
Storage::disk('public')->put($path.$file_name, $tempStream);
Storage::put($path.$file_name, file_get_contents($tmp_file));
fclose($tempStream);
$nmo = new NinjaMailerObject; $nmo = new NinjaMailerObject;
$nmo->mailable = new DownloadInvoices(Storage::disk('public')->url($path.$file_name), $this->company); $nmo->mailable = new DownloadInvoices(Storage::url($path.$file_name), $this->company);
$nmo->to_user = $this->user; $nmo->to_user = $this->user;
$nmo->settings = $this->settings; $nmo->settings = $this->settings;
$nmo->company = $this->company; $nmo->company = $this->company;
NinjaMailerJob::dispatch($nmo); NinjaMailerJob::dispatch($nmo);
UnlinkFile::dispatch('public', $path.$file_name)->delay(now()->addHours(1)); UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1));
} }
} }

View File

@ -71,10 +71,10 @@ class NinjaMailerJob implements ShouldQueue
public function handle() public function handle()
{ {
/*If we are migrating data we don't want to fire any emails*/
if ($this->nmo->company->is_disabled && !$this->override)
return true;
if($this->preFlightChecksFail())
return;
/*Set the correct database*/ /*Set the correct database*/
MultiDB::setDb($this->nmo->company->db); MultiDB::setDb($this->nmo->company->db);
@ -215,6 +215,19 @@ class NinjaMailerJob implements ShouldQueue
} }
private function preFlightChecksFail()
{
/* If we are migrating data we don't want to fire any emails */
if ($this->nmo->company->is_disabled && !$this->override)
return true;
/* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */
if(Ninja::isHosted() && strpos($this->nmo->to_user->email, '@example.com') !== false)
return true;
return false;
}
private function logMailError($errors, $recipient_object) private function logMailError($errors, $recipient_object)
{ {
SystemLogger::dispatch( SystemLogger::dispatch(

View File

@ -56,7 +56,7 @@ class CompanySizeCheck implements ShouldQueue
{ {
Company::cursor()->each(function ($company) { Company::cursor()->each(function ($company) {
if ($company->invoices()->count() > 1000 || $company->products()->count() > 1000 || $company->clients()->count() > 1000) { if ($company->invoices()->count() > 500 || $company->products()->count() > 500 || $company->clients()->count() > 500) {
nlog("Marking company {$company->id} as large"); nlog("Marking company {$company->id} as large");

View File

@ -29,6 +29,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
//@DEPRECATED
class SendReminders implements ShouldQueue class SendReminders implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesDates, MakesReminders; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesDates, MakesReminders;

View File

@ -214,7 +214,7 @@ class Import implements ShouldQueue
// if(Ninja::isHosted() && array_key_exists('ninja_tokens', $data)) // if(Ninja::isHosted() && array_key_exists('ninja_tokens', $data))
$this->processNinjaTokens($data['ninja_tokens']); $this->processNinjaTokens($data['ninja_tokens']);
$this->fixData(); // $this->fixData();
$this->setInitialCompanyLedgerBalances(); $this->setInitialCompanyLedgerBalances();
@ -393,6 +393,10 @@ class Import implements ShouldQueue
foreach ($data['settings'] as $key => $value) { foreach ($data['settings'] as $key => $value) {
if ($key == 'invoice_design_id' || $key == 'quote_design_id' || $key == 'credit_design_id') { if ($key == 'invoice_design_id' || $key == 'quote_design_id' || $key == 'credit_design_id') {
$value = $this->encodePrimaryKey($value); $value = $this->encodePrimaryKey($value);
if(!$value)
$value = $this->encodePrimaryKey(1);
} }
if ($key == 'payment_terms' && $key = '') { if ($key == 'payment_terms' && $key = '') {

View File

@ -11,11 +11,13 @@
namespace App\Jobs\Util; namespace App\Jobs\Util;
use App\DataMapper\InvoiceItem;
use App\Events\Invoice\InvoiceWasEmailed; use App\Events\Invoice\InvoiceWasEmailed;
use App\Jobs\Entity\EmailEntity; use App\Jobs\Entity\EmailEntity;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\Invoice; use App\Models\Invoice;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesReminders; use App\Utils\Traits\MakesReminders;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -26,7 +28,7 @@ use Illuminate\Support\Carbon;
class ReminderJob implements ShouldQueue class ReminderJob implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesReminders; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesReminders, MakesDates;
public function __construct() public function __construct()
{ {
@ -65,6 +67,8 @@ class ReminderJob implements ShouldQueue
if ($invoice->isPayable()) { if ($invoice->isPayable()) {
$reminder_template = $invoice->calculateTemplate('invoice'); $reminder_template = $invoice->calculateTemplate('invoice');
$invoice->service()->touchReminder($reminder_template)->save(); $invoice->service()->touchReminder($reminder_template)->save();
$invoice = $this->calcLateFee($invoice, $reminder_template);
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) { $invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
@ -84,4 +88,89 @@ class ReminderJob implements ShouldQueue
}); });
} }
/**
* Calculates the late if - if any - and rebuilds the invoice
*
* @param Invoice $invoice
* @param string $template
* @return Invoice
*/
private function calcLateFee($invoice, $template) :Invoice
{
$late_fee_amount = 0;
$late_fee_percent = 0;
switch ($template) {
case 'reminder1':
$late_fee_amount = $invoice->client->getSetting('late_fee_amount1');
$late_fee_percent = $invoice->client->getSetting('late_fee_percent1');
break;
case 'reminder2':
$late_fee_amount = $invoice->client->getSetting('late_fee_amount2');
$late_fee_percent = $invoice->client->getSetting('late_fee_percent2');
break;
case 'reminder3':
$late_fee_amount = $invoice->client->getSetting('late_fee_amount3');
$late_fee_percent = $invoice->client->getSetting('late_fee_percent3');
break;
case 'endless_reminder':
$late_fee_amount = $invoice->client->getSetting('late_fee_endless_amount');
$late_fee_percent = $invoice->client->getSetting('late_fee_endless_percent');
break;
default:
$late_fee_amount = 0;
$late_fee_percent = 0;
break;
}
return $this->setLateFee($invoice, $late_fee_amount, $late_fee_percent);
}
/**
* Applies the late fee to the invoice line items
*
* @param Invoice $invoice
* @param float $amount The fee amount
* @param float $percent The fee percentage amount
*
* @return Invoice
*/
private function setLateFee($invoice, $amount, $percent) :Invoice
{
$temp_invoice_balance = $invoice->balance;
if ($amount <= 0 && $percent <= 0) {
return $invoice;
}
$fee = $amount;
if ($invoice->partial > 0) {
$fee += round($invoice->partial * $percent / 100, 2);
} else {
$fee += round($invoice->balance * $percent / 100, 2);
}
$invoice_item = new InvoiceItem;
$invoice_item->type_id = '5';
$invoice_item->product_key = trans('texts.fee');
$invoice_item->notes = ctrans('texts.late_fee_added', ['date' => $this->translateDate(now()->startOfDay(), $invoice->client->date_format(), $invoice->client->locale())]);
$invoice_item->quantity = 1;
$invoice_item->cost = $fee;
$invoice_items = $invoice->line_items;
$invoice_items[] = $invoice_item;
$invoice->line_items = $invoice_items;
/**Refresh Invoice values*/
$invoice = $invoice->calc()->getInvoice();
$invoice->client->service()->updateBalance($this->invoice->balance - $temp_invoice_balance)->save();
$invoice->ledger()->updateInvoiceBalance($this->invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$this->invoice->number}");
return $invoice;
}
} }

View File

@ -92,16 +92,6 @@ class StartMigration implements ShouldQueue
$archive = $zip->open(public_path("storage/{$this->filepath}")); $archive = $zip->open(public_path("storage/{$this->filepath}"));
$filename = pathinfo($this->filepath, PATHINFO_FILENAME); $filename = pathinfo($this->filepath, PATHINFO_FILENAME);
// if($this->company->id == $this->company->account->default_company_id)
// {
// $new_default_company = $this->company->account->companies->first();
// if ($new_default_company) {
// $this->company->account->default_company_id = $new_default_company->id;
// $this->company->account->save();
// }
// }
$update_product_flag = $this->company->update_products; $update_product_flag = $this->company->update_products;
$this->company->update_products = false; $this->company->update_products = false;
@ -129,9 +119,6 @@ class StartMigration implements ShouldQueue
Storage::deleteDirectory(public_path("storage/migrations/{$filename}")); Storage::deleteDirectory(public_path("storage/migrations/{$filename}"));
// $this->company->account->default_company_id = $this->company->id;
// $this->company->account->save();
$this->company->update_products = $update_product_flag; $this->company->update_products = $update_product_flag;
$this->company->save(); $this->company->save();

View File

@ -15,6 +15,7 @@ use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Company; use App\Models\Company;
use App\Models\CompanyToken; use App\Models\CompanyToken;
use App\Models\Document;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -206,6 +207,24 @@ class MultiDB
return false; return false;
} }
public static function documentFindAndSetDb($hash) : bool
{
$current_db = config('database.default');
//multi-db active
foreach (self::$dbs as $db) {
if (Document::on($db)->where('hash', $hash)->count() >= 1){
self::setDb($db);
return true;
}
}
self::setDB($current_db);
return false;
}
public static function findAndSetDb($token) :bool public static function findAndSetDb($token) :bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');

View File

@ -43,7 +43,7 @@ class RestoredUserActivity implements ShouldQueue
$fields = new stdClass; $fields = new stdClass;
$fields->user_id = $creating_user->user->id; $fields->user_id = $event->user->id;
$fields->notes = $event->creating_user->present()->name() . " Restored user " . $event->user->present()->name(); $fields->notes = $event->creating_user->present()->name() . " Restored user " . $event->user->present()->name();
$fields->company_id = $event->company->id; $fields->company_id = $event->company->id;

View File

@ -11,6 +11,9 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Utils\Ninja;
use Illuminate\Support\Facades\App;
class AccountCreatedObject class AccountCreatedObject
{ {
@ -30,6 +33,14 @@ class AccountCreatedObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$data = [ $data = [
'title' => ctrans('texts.new_signup'), 'title' => ctrans('texts.new_signup'),
'message' => ctrans('texts.new_signup_text', ['user' => $this->user->present()->name(), 'email' => $this->user->email, 'ip' => $this->user->ip]), 'message' => ctrans('texts.new_signup_text', ['user' => $this->user->present()->name(), 'email' => $this->user->email, 'ip' => $this->user->ip]),

View File

@ -13,8 +13,10 @@ namespace App\Mail\Admin;
use App\Models\Invoice; use App\Models\Invoice;
use App\Utils\HtmlEngine; use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\App;
use stdClass; use stdClass;
class AutoBillingFailureObject class AutoBillingFailureObject
@ -55,6 +57,15 @@ class AutoBillingFailureObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$this->$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get(); $this->$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$mail_obj = new stdClass; $mail_obj = new stdClass;

View File

@ -13,8 +13,10 @@ namespace App\Mail\Admin;
use App\Models\Invoice; use App\Models\Invoice;
use App\Utils\HtmlEngine; use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\App;
use stdClass; use stdClass;
class ClientPaymentFailureObject class ClientPaymentFailureObject
@ -56,6 +58,14 @@ class ClientPaymentFailureObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get(); $this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$mail_obj = new stdClass; $mail_obj = new stdClass;

View File

@ -11,8 +11,10 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use stdClass; use stdClass;
use Illuminate\Support\Facades\App;
class EntityCreatedObject class EntityCreatedObject
{ {
@ -39,6 +41,13 @@ class EntityCreatedObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->entity->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->entity->company->settings));
$this->contact = $this->entity->invitations()->first()->contact; $this->contact = $this->entity->invitations()->first()->contact;
$this->company = $this->entity->company; $this->company = $this->entity->company;

View File

@ -12,8 +12,10 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Utils\HtmlEngine; use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use stdClass; use stdClass;
use Illuminate\Support\Facades\App;
class EntityFailedSendObject class EntityFailedSendObject
{ {
@ -50,6 +52,15 @@ class EntityFailedSendObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$this->setTemplate(); $this->setTemplate();
$mail_obj = new stdClass; $mail_obj = new stdClass;

View File

@ -12,7 +12,9 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Mail\Engine\PaymentEmailEngine; use App\Mail\Engine\PaymentEmailEngine;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use Illuminate\Support\Facades\App;
use stdClass; use stdClass;
class EntityPaidObject class EntityPaidObject
@ -35,6 +37,15 @@ class EntityPaidObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$mail_obj = new stdClass; $mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount(); $mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject(); $mail_obj->subject = $this->getSubject();

View File

@ -11,8 +11,10 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use stdClass; use stdClass;
use Illuminate\Support\Facades\App;
class EntitySentObject class EntitySentObject
{ {
@ -46,6 +48,15 @@ class EntitySentObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$this->setTemplate(); $this->setTemplate();
$mail_obj = new stdClass; $mail_obj = new stdClass;

View File

@ -11,8 +11,10 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use stdClass; use stdClass;
use Illuminate\Support\Facades\App;
class EntityViewedObject class EntityViewedObject
{ {
@ -39,6 +41,15 @@ class EntityViewedObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$mail_obj = new stdClass; $mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount(); $mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject(); $mail_obj->subject = $this->getSubject();

View File

@ -12,9 +12,11 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Models\Invoice; use App\Models\Invoice;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use stdClass; use stdClass;
use Illuminate\Support\Facades\App;
class PaymentFailureObject class PaymentFailureObject
{ {
@ -55,6 +57,14 @@ class PaymentFailureObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
// $this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get(); // $this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$mail_obj = new stdClass; $mail_obj = new stdClass;

View File

@ -11,6 +11,9 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Utils\Ninja;
use Illuminate\Support\Facades\App;
class ResetPasswordObject class ResetPasswordObject
{ {
@ -32,6 +35,14 @@ class ResetPasswordObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$data = [ $data = [
'title' => ctrans('texts.your_password_reset_link'), 'title' => ctrans('texts.your_password_reset_link'),
'message' => ctrans('texts.reset_password'), 'message' => ctrans('texts.reset_password'),

View File

@ -11,7 +11,9 @@
namespace App\Mail\Admin; namespace App\Mail\Admin;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\App;
class VerifyUserObject class VerifyUserObject
{ {
@ -33,6 +35,15 @@ class VerifyUserObject
public function build() public function build()
{ {
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$this->user->confirmation_code = $this->createDbHash($this->company->db); $this->user->confirmation_code = $this->createDbHash($this->company->db);
$this->user->save(); $this->user->save();

View File

@ -11,6 +11,9 @@
namespace App\Mail\ClientContact; namespace App\Mail\ClientContact;
use App\Utils\Ninja;
use Illuminate\Support\Facades\App;
class ClientContactResetPasswordObject class ClientContactResetPasswordObject
{ {
@ -32,6 +35,12 @@ class ClientContactResetPasswordObject
public function build() public function build()
{ {
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($this->client_contact->preferredLocale());
$t->replace(Ninja::transformTranslations($this->client_contact->client->getMergedSettings()));
$data = [ $data = [
'title' => ctrans('texts.your_password_reset_link'), 'title' => ctrans('texts.your_password_reset_link'),
'content' => ctrans('texts.reset_password'), 'content' => ctrans('texts.reset_password'),

View File

@ -17,6 +17,7 @@ use App\Utils\Helpers;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use Illuminate\Support\Facades\App;
class PaymentEmailEngine extends BaseEmailEngine class PaymentEmailEngine extends BaseEmailEngine
{ {
@ -49,6 +50,11 @@ class PaymentEmailEngine extends BaseEmailEngine
public function build() public function build()
{ {
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($this->contact->preferredLocale());
$t->replace(Ninja::transformTranslations($this->client->getMergedSettings()));
if (is_array($this->template_data) && array_key_exists('body', $this->template_data) && strlen($this->template_data['body']) > 0) { if (is_array($this->template_data) && array_key_exists('body', $this->template_data) && strlen($this->template_data['body']) > 0) {
$body_template = $this->template_data['body']; $body_template = $this->template_data['body'];
} elseif (strlen($this->client->getSetting('email_template_payment')) > 0) { } elseif (strlen($this->client->getSetting('email_template_payment')) > 0) {

View File

@ -11,6 +11,9 @@
namespace App\Mail\RecurringInvoice; namespace App\Mail\RecurringInvoice;
use App\Utils\Ninja;
use Illuminate\Support\Facades\App;
class ClientContactRequestCancellationObject class ClientContactRequestCancellationObject
{ {
@ -33,6 +36,10 @@ class ClientContactRequestCancellationObject
public function build() public function build()
{ {
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
$data = [ $data = [
'title' => ctrans('texts.recurring_cancellation_request', ['contact' => $this->client_contact->present()->name()]), 'title' => ctrans('texts.recurring_cancellation_request', ['contact' => $this->client_contact->present()->name()]),
'content' => ctrans('texts.recurring_cancellation_request_body', ['contact' => $this->client_contact->present()->name(), 'client' => $this->client_contact->client->present()->name(), 'invoice' => $this->recurring_invoice->number]), 'content' => ctrans('texts.recurring_cancellation_request_body', ['contact' => $this->client_contact->present()->name(), 'client' => $this->client_contact->client->present()->name(), 'invoice' => $this->recurring_invoice->number]),

View File

@ -58,9 +58,9 @@ class SupportMessageSent extends Mailable
$user = auth()->user(); $user = auth()->user();
if(Ninja::isHosted()) if(Ninja::isHosted())
$subject = "Hosted {$user->present()->name} - [{$plan} - DB:{$company->db}]"; $subject = "Hosted {$user->present()->name} - [{$plan} - {$company->db}]";
else else
$subject = "Self Host {$user->present()->name} - [{$plan} - DB:{$company->db}]"; $subject = "Self Host {$user->present()->name} - [{$plan} - {$company->db}]";
return $this->from(config('mail.from.address'), config('mail.from.name')) return $this->from(config('mail.from.address'), config('mail.from.name'))
->replyTo($user->email, $user->present()->name()) ->replyTo($user->email, $user->present()->name())

View File

@ -227,6 +227,21 @@ class Account extends BaseModel
return $plan_details && $plan_details['trial']; return $plan_details && $plan_details['trial'];
} }
public function startTrial($plan)
{
if (! Ninja::isNinja()) {
return;
}
if ($this->trial_started && $this->trial_started != '0000-00-00') {
return;
}
$this->trial_plan = $plan;
$this->trial_started = now();
$this->save();
}
public function getPlanDetails($include_inactive = false, $include_trial = true) public function getPlanDetails($include_inactive = false, $include_trial = true)
{ {
$plan = $this->plan; $plan = $this->plan;

View File

@ -215,6 +215,11 @@ class Client extends BaseModel implements HasLocalePreference
return $this->hasMany(Invoice::class)->withTrashed(); return $this->hasMany(Invoice::class)->withTrashed();
} }
public function recurring_invoices()
{
return $this->hasMany(RecurringInvoice::class)->withTrashed();
}
public function shipping_country() public function shipping_country()
{ {
return $this->belongsTo(Country::class, 'shipping_country_id', 'id'); return $this->belongsTo(Country::class, 'shipping_country_id', 'id');
@ -407,7 +412,7 @@ class Client extends BaseModel implements HasLocalePreference
} }
foreach ($gateways as $gateway) { foreach ($gateways as $gateway) {
if (in_array(GatewayType::CREDIT_CARD, $gateway->driver($this)->gatewayTypes())) { if (in_array(GatewayType::CREDIT_CARD, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::CREDIT_CARD))) {
return $gateway; return $gateway;
} }
} }
@ -432,11 +437,11 @@ class Client extends BaseModel implements HasLocalePreference
} }
foreach ($gateways as $gateway) { foreach ($gateways as $gateway) {
if ($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, $gateway->driver($this)->gatewayTypes())) { if ($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::BANK_TRANSFER))) {
return $gateway; return $gateway;
} }
if ($this->currency()->code == 'EUR' && in_array(GatewayType::SEPA, $gateway->driver($this)->gatewayTypes())) { if ($this->currency()->code == 'EUR' && in_array(GatewayType::SEPA, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::SEPA))) {
return $gateway; return $gateway;
} }
} }

View File

@ -35,6 +35,7 @@ class ClientGatewayToken extends BaseModel
'gateway_customer_reference', 'gateway_customer_reference',
'gateway_type_id', 'gateway_type_id',
'meta', 'meta',
'client_id',
]; ];
public function getEntityType() public function getEntityType()

View File

@ -11,6 +11,7 @@
namespace App\Models; namespace App\Models;
use App\Models\Language;
use App\Models\Presenters\CompanyPresenter; use App\Models\Presenters\CompanyPresenter;
use App\Models\User; use App\Models\User;
use App\Services\Notification\NotificationService; use App\Services\Notification\NotificationService;

View File

@ -31,6 +31,7 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
use Illuminate\Support\Facades\Cache;
class User extends Authenticatable implements MustVerifyEmail class User extends Authenticatable implements MustVerifyEmail
{ {

View File

@ -54,11 +54,11 @@ class AuthorizePaymentDriver extends BaseDriver
/** /**
* Returns the gateway types. * Returns the gateway types.
*/ */
public function gatewayTypes() :array public function gatewayTypes(): array
{ {
$types = [ $types = [];
GatewayType::CREDIT_CARD,
]; $types[] = GatewayType::CREDIT_CARD;
return $types; return $types;
} }

View File

@ -26,6 +26,7 @@ use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash; use App\Models\PaymentHash;
@ -546,4 +547,24 @@ class BaseDriver extends AbstractPaymentDriver
$this->client->company, $this->client->company,
); );
} }
/* Performs an extra iterate on the gatewayTypes() array and passes back only the enabled gateways*/
public function gatewayTypeEnabled($type)
{
$types = [];
// if($type == GatewayType::BANK_TRANSFER && $this->company_gateway->fees_and_limits->{GatewayType::BANK_TRANSFER}->is_enabled)
// {
// $types[] = $type;
// }
// elseif($type == GatewayType::CREDIT_CARD && $this->company_gateway->fees_and_limits->{GatewayType::CREDIT_CARD}->is_enabled)
// {
// $types[] = $type;
// }
$types[] = GatewayType::CREDIT_CARD;
$types[] = GatewayType::BANK_TRANSFER;
return $types;
}
} }

View File

@ -25,6 +25,8 @@ use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\Braintree\CreditCard; use App\PaymentDrivers\Braintree\CreditCard;
use App\PaymentDrivers\Braintree\PayPal; use App\PaymentDrivers\Braintree\PayPal;
use Braintree\Gateway;
use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class BraintreePaymentDriver extends BaseDriver class BraintreePaymentDriver extends BaseDriver
@ -36,7 +38,7 @@ class BraintreePaymentDriver extends BaseDriver
public $can_authorise_credit_card = true; public $can_authorise_credit_card = true;
/** /**
* @var \Braintree\Gateway; * @var Gateway;
*/ */
public $gateway; public $gateway;
@ -49,7 +51,7 @@ class BraintreePaymentDriver extends BaseDriver
public function init(): void public function init(): void
{ {
$this->gateway = new \Braintree\Gateway([ $this->gateway = new Gateway([
'environment' => $this->company_gateway->getConfigField('testMode') ? 'sandbox' : 'production', 'environment' => $this->company_gateway->getConfigField('testMode') ? 'sandbox' : 'production',
'merchantId' => $this->company_gateway->getConfigField('merchantId'), 'merchantId' => $this->company_gateway->getConfigField('merchantId'),
'publicKey' => $this->company_gateway->getConfigField('publicKey'), 'publicKey' => $this->company_gateway->getConfigField('publicKey'),
@ -68,10 +70,12 @@ class BraintreePaymentDriver extends BaseDriver
public function gatewayTypes(): array public function gatewayTypes(): array
{ {
return [ $types = [
GatewayType::CREDIT_CARD,
GatewayType::PAYPAL, GatewayType::PAYPAL,
GatewayType::CREDIT_CARD
]; ];
return $types;
} }
public function authorizeView($data) public function authorizeView($data)
@ -126,11 +130,11 @@ class BraintreePaymentDriver extends BaseDriver
return [ return [
'transaction_reference' => $response->id, 'transaction_reference' => $response->id,
'transaction_response' => json_encode($response), 'transaction_response' => json_encode($response),
'success' => (bool) $response->success, 'success' => (bool)$response->success,
'description' => $response->status, 'description' => $response->status,
'code' => 0, 'code' => 0,
]; ];
} catch (\Exception $e) { } catch (Exception $e) {
return [ return [
'transaction_reference' => null, 'transaction_reference' => null,
'transaction_response' => json_encode($e->getMessage()), 'transaction_response' => json_encode($e->getMessage()),
@ -174,7 +178,7 @@ class BraintreePaymentDriver extends BaseDriver
'gateway_type_id' => GatewayType::CREDIT_CARD, 'gateway_type_id' => GatewayType::CREDIT_CARD,
]; ];
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); $payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
SystemLogger::dispatch( SystemLogger::dispatch(
['response' => $result, 'data' => $data], ['response' => $result, 'data' => $data],

View File

@ -72,11 +72,13 @@ class CheckoutComPaymentDriver extends BaseDriver
/** /**
* Returns the default gateway type. * Returns the default gateway type.
*/ */
public function gatewayTypes() public function gatewayTypes(): array
{ {
return [ $types = [];
GatewayType::CREDIT_CARD,
]; $types[] = GatewayType::CREDIT_CARD;
return $types;
} }
/** /**
@ -232,7 +234,7 @@ class CheckoutComPaymentDriver extends BaseDriver
'transaction_reference' => $response->id, 'transaction_reference' => $response->id,
]; ];
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); $payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
SystemLogger::dispatch( SystemLogger::dispatch(
['response' => $response, 'data' => $data], ['response' => $response, 'data' => $data],
@ -269,11 +271,11 @@ class CheckoutComPaymentDriver extends BaseDriver
return false; return false;
} }
} catch (\Exception | CheckoutHttpException $e) { } catch (Exception | CheckoutHttpException $e) {
$this->unWindGatewayFees($payment_hash); $this->unWindGatewayFees($payment_hash);
$message = $e instanceof CheckoutHttpException $message = $e instanceof CheckoutHttpException
? $e->getBody() ? $e->getBody()
: $e->getMessage(); : $e->getMessage();
$data = [ $data = [
'status' => '', 'status' => '',

View File

@ -128,7 +128,7 @@ class ACH
$data['currency'] = $this->stripe->client->getCurrencyCode(); $data['currency'] = $this->stripe->client->getCurrencyCode();
$data['payment_method_id'] = GatewayType::BANK_TRANSFER; $data['payment_method_id'] = GatewayType::BANK_TRANSFER;
$data['customer'] = $this->stripe->findOrCreateCustomer(); $data['customer'] = $this->stripe->findOrCreateCustomer();
$data['amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision); $data['amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
return render('gateways.stripe.ach.pay', $data); return render('gateways.stripe.ach.pay', $data);
} }
@ -151,7 +151,7 @@ class ACH
$state = [ $state = [
'payment_method' => $request->payment_method_id, 'payment_method' => $request->payment_method_id,
'gateway_type_id' => $request->company_gateway_id, 'gateway_type_id' => $request->company_gateway_id,
'amount' => $this->stripe->convertToStripeAmount($request->amount, $this->stripe->client->currency()->precision), 'amount' => $this->stripe->convertToStripeAmount($request->amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'currency' => $request->currency, 'currency' => $request->currency,
'customer' => $request->customer, 'customer' => $request->customer,
]; ];
@ -196,7 +196,7 @@ class ACH
$data = [ $data = [
'payment_method' => $state['source'], 'payment_method' => $state['source'],
'payment_type' => PaymentType::ACH, 'payment_type' => PaymentType::ACH,
'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->amount, $this->stripe->client->currency()->precision), 'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'transaction_reference' => $state['charge']->id, 'transaction_reference' => $state['charge']->id,
'gateway_type_id' => GatewayType::BANK_TRANSFER, 'gateway_type_id' => GatewayType::BANK_TRANSFER,
]; ];

View File

@ -37,7 +37,7 @@ class Alipay
$data['gateway'] = $this->stripe; $data['gateway'] = $this->stripe;
$data['return_url'] = $this->buildReturnUrl(); $data['return_url'] = $this->buildReturnUrl();
$data['currency'] = $this->stripe->client->getCurrencyCode(); $data['currency'] = $this->stripe->client->getCurrencyCode();
$data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision); $data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
$data['invoices'] = $this->stripe->payment_hash->invoices(); $data['invoices'] = $this->stripe->payment_hash->invoices();
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]); $this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]);
@ -74,7 +74,7 @@ class Alipay
$data = [ $data = [
'payment_method' => $this->stripe->payment_hash->data->source, 'payment_method' => $this->stripe->payment_hash->data->source,
'payment_type' => PaymentType::ALIPAY, 'payment_type' => PaymentType::ALIPAY,
'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision), 'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'transaction_reference' => $source, 'transaction_reference' => $source,
'gateway_type_id' => GatewayType::ALIPAY, 'gateway_type_id' => GatewayType::ALIPAY,
@ -104,7 +104,7 @@ class Alipay
$this->stripe->client, $this->stripe->client,
$server_response, $server_response,
$this->stripe->client->company, $this->stripe->client->company,
$this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision) $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency())
); );
$message = [ $message = [

View File

@ -68,7 +68,7 @@ class Charge
try { try {
$data = [ $data = [
'amount' => $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision), 'amount' => $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'currency' => $this->stripe->client->getCurrencyCode(), 'currency' => $this->stripe->client->getCurrencyCode(),
'payment_method' => $cgt->token, 'payment_method' => $cgt->token,
'customer' => $cgt->gateway_customer_reference, 'customer' => $cgt->gateway_customer_reference,

View File

@ -60,7 +60,7 @@ class CreditCard
public function paymentView(array $data) public function paymentView(array $data)
{ {
$payment_intent_data = [ $payment_intent_data = [
'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision), 'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'currency' => $this->stripe->client->getCurrencyCode(), 'currency' => $this->stripe->client->getCurrencyCode(),
'customer' => $this->stripe->findOrCreateCustomer(), 'customer' => $this->stripe->findOrCreateCustomer(),
'description' => ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number'), // TODO: More meaningful description. 'description' => ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number'), // TODO: More meaningful description.
@ -115,7 +115,7 @@ class CreditCard
$data = [ $data = [
'payment_method' => $this->stripe->payment_hash->data->server_response->payment_method, 'payment_method' => $this->stripe->payment_hash->data->server_response->payment_method,
'payment_type' => PaymentType::parseCardType(strtolower($stripe_method->card->brand)), 'payment_type' => PaymentType::parseCardType(strtolower($stripe_method->card->brand)),
'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->server_response->amount, $this->stripe->client->currency()->precision), 'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->server_response->amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'transaction_reference' => optional($this->stripe->payment_hash->data->payment_intent->charges->data[0])->id, 'transaction_reference' => optional($this->stripe->payment_hash->data->payment_intent->charges->data[0])->id,
'gateway_type_id' => GatewayType::CREDIT_CARD, 'gateway_type_id' => GatewayType::CREDIT_CARD,
]; ];

View File

@ -40,7 +40,7 @@ class SOFORT
{ {
$data['gateway'] = $this->stripe; $data['gateway'] = $this->stripe;
$data['return_url'] = $this->buildReturnUrl(); $data['return_url'] = $this->buildReturnUrl();
$data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision); $data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
$data['client'] = $this->stripe->client; $data['client'] = $this->stripe->client;
$data['country'] = $this->stripe->client->country->iso_3166_2; $data['country'] = $this->stripe->client->country->iso_3166_2;
@ -80,7 +80,7 @@ class SOFORT
$data = [ $data = [
'payment_method' => $this->stripe->payment_hash->data->source, 'payment_method' => $this->stripe->payment_hash->data->source,
'payment_type' => PaymentType::SOFORT, 'payment_type' => PaymentType::SOFORT,
'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision), 'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'transaction_reference' => $source, 'transaction_reference' => $source,
'gateway_type_id' => GatewayType::SOFORT, 'gateway_type_id' => GatewayType::SOFORT,
]; ];
@ -107,7 +107,7 @@ class SOFORT
$this->stripe->client, $this->stripe->client,
$server_response, $server_response,
$this->stripe->client->company, $this->stripe->client->company,
$this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision) $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency())
); );
$message = [ $message = [

View File

@ -14,13 +14,24 @@ namespace App\PaymentDrivers\Stripe;
trait Utilities trait Utilities
{ {
public function convertFromStripeAmount($amount, $precision) /*Helpers for currency conversions, NOTE* for some currencies we need to change behaviour */
public function convertFromStripeAmount($amount, $precision, $currency)
{ {
if($currency->code == "JPY")
return $amount;
return $amount / pow(10, $precision); return $amount / pow(10, $precision);
} }
public function convertToStripeAmount($amount, $precision) public function convertToStripeAmount($amount, $precision, $currency)
{ {
return (int)($amount * pow(10, $precision));
if($currency->code == "JPY")
return $amount;
return round(($amount * pow(10, $precision)),0);
} }
} }

View File

@ -32,7 +32,9 @@ use App\PaymentDrivers\Stripe\UpdatePaymentMethods;
use App\PaymentDrivers\Stripe\Utilities; use App\PaymentDrivers\Stripe\Utilities;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Exception; use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Laracasts\Presenter\Exceptions\PresenterException;
use Stripe\Account; use Stripe\Account;
use Stripe\Customer; use Stripe\Customer;
use Stripe\Exception\ApiErrorException; use Stripe\Exception\ApiErrorException;
@ -52,7 +54,7 @@ class StripePaymentDriver extends BaseDriver
public $can_authorise_credit_card = true; public $can_authorise_credit_card = true;
/** @var \Stripe\StripeClient */ /** @var StripeClient */
public $stripe; public $stripe;
protected $customer_reference = 'customerReferenceParam'; protected $customer_reference = 'customerReferenceParam';
@ -112,11 +114,9 @@ class StripePaymentDriver extends BaseDriver
public function gatewayTypes(): array public function gatewayTypes(): array
{ {
$types = [ $types = [
GatewayType::CREDIT_CARD,
GatewayType::CRYPTO, GatewayType::CRYPTO,
// GatewayType::SEPA, // TODO: Missing implementation GatewayType::CREDIT_CARD
// GatewayType::APPLE_PAY, // TODO:: Missing implementation ];
];
if ($this->client if ($this->client
&& isset($this->client->country) && isset($this->client->country)
@ -126,7 +126,8 @@ class StripePaymentDriver extends BaseDriver
if ($this->client if ($this->client
&& isset($this->client->country) && isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['USA'])) { && in_array($this->client->country->iso_3166_3, ['USA'])
) {
$types[] = GatewayType::BANK_TRANSFER; $types[] = GatewayType::BANK_TRANSFER;
} }
@ -167,18 +168,12 @@ class StripePaymentDriver extends BaseDriver
public function getClientRequiredFields(): array public function getClientRequiredFields(): array
{ {
$fields = [ $fields = [];
['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'],
];
if ($this->company_gateway->require_client_name) { if ($this->company_gateway->require_client_name) {
$fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required'];
} }
if ($this->company_gateway->require_client_phone) {
$fields[] = ['name' => 'client_phone', 'label' => ctrans('texts.client_phone'), 'type' => 'tel', 'validation' => 'required'];
}
if ($this->company_gateway->require_contact_name) { if ($this->company_gateway->require_contact_name) {
$fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required'];
@ -188,6 +183,10 @@ class StripePaymentDriver extends BaseDriver
$fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc']; $fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc'];
} }
if ($this->company_gateway->require_client_phone) {
$fields[] = ['name' => 'client_phone', 'label' => ctrans('texts.client_phone'), 'type' => 'tel', 'validation' => 'required'];
}
if ($this->company_gateway->require_billing_address) { if ($this->company_gateway->require_billing_address) {
$fields[] = ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'];
// $fields[] = ['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'nullable']; // $fields[] = ['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'nullable'];
@ -196,6 +195,8 @@ class StripePaymentDriver extends BaseDriver
$fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required'];
} }
$fields[] = ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'];
if ($this->company_gateway->require_shipping_address) { if ($this->company_gateway->require_shipping_address) {
$fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required'];
// $fields[] = ['name' => 'client_shipping_address_line_2', 'label' => ctrans('texts.shipping_address2'), 'type' => 'text', 'validation' => 'sometimes']; // $fields[] = ['name' => 'client_shipping_address_line_2', 'label' => ctrans('texts.shipping_address2'), 'type' => 'text', 'validation' => 'sometimes'];
@ -205,6 +206,7 @@ class StripePaymentDriver extends BaseDriver
$fields[] = ['name' => 'client_shipping_country_id', 'label' => ctrans('texts.shipping_country'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'client_shipping_country_id', 'label' => ctrans('texts.shipping_country'), 'type' => 'text', 'validation' => 'required'];
} }
return $fields; return $fields;
} }
@ -212,7 +214,7 @@ class StripePaymentDriver extends BaseDriver
* Proxy method to pass the data into payment method authorizeView(). * Proxy method to pass the data into payment method authorizeView().
* *
* @param array $data * @param array $data
* @return \Illuminate\Http\RedirectResponse|mixed * @return RedirectResponse|mixed
*/ */
public function authorizeView(array $data) public function authorizeView(array $data)
{ {
@ -223,7 +225,7 @@ class StripePaymentDriver extends BaseDriver
* Processes the gateway response for credit card authorization. * Processes the gateway response for credit card authorization.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|mixed * @return RedirectResponse|mixed
*/ */
public function authorizeResponse($request) public function authorizeResponse($request)
{ {
@ -234,7 +236,7 @@ class StripePaymentDriver extends BaseDriver
* Process the payment with gateway. * Process the payment with gateway.
* *
* @param array $data * @param array $data
* @return \Illuminate\Http\RedirectResponse|mixed * @return RedirectResponse|mixed
*/ */
public function processPaymentView(array $data) public function processPaymentView(array $data)
{ {
@ -292,7 +294,7 @@ class StripePaymentDriver extends BaseDriver
* Finds or creates a Stripe Customer object. * Finds or creates a Stripe Customer object.
* *
* @return null|Customer A Stripe customer object * @return null|Customer A Stripe customer object
* @throws \Laracasts\Presenter\Exceptions\PresenterException * @throws PresenterException
* @throws ApiErrorException * @throws ApiErrorException
*/ */
public function findOrCreateCustomer(): ?Customer public function findOrCreateCustomer(): ?Customer
@ -336,7 +338,7 @@ class StripePaymentDriver extends BaseDriver
try { try {
$response = $this->stripe $response = $this->stripe
->refunds ->refunds
->create(['charge' => $payment->transaction_reference, 'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision)], $meta); ->create(['charge' => $payment->transaction_reference, 'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision, $this->client->currency())], $meta);
if ($response->status == $response::STATUS_SUCCEEDED) { if ($response->status == $response::STATUS_SUCCEEDED) {
SystemLogger::dispatch(['server_response' => $response, 'data' => request()->all(),], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->client, $this->client->company); SystemLogger::dispatch(['server_response' => $response, 'data' => request()->all(),], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->client, $this->client->company);

View File

@ -81,11 +81,8 @@ class WePayPaymentDriver extends BaseDriver
{ {
$types = []; $types = [];
if($this->company_gateway->fees_and_limits->{GatewayType::BANK_TRANSFER}->is_enabled) $types[] = GatewayType::CREDIT_CARD;
$types[] = GatewayType::CREDIT_CARD; $types[] = GatewayType::BANK_TRANSFER;
if($this->company_gateway->fees_and_limits->{GatewayType::BANK_TRANSFER}->is_enabled)
$types[] = GatewayType::BANK_TRANSFER;
return $types; return $types;
} }

View File

@ -48,6 +48,25 @@ class CompanyRepository extends BaseRepository
private function parseCustomFields($fields) :array private function parseCustomFields($fields) :array
{ {
if(array_key_exists('account1', $fields))
$fields['company1'] = $fields['account1'];
if(array_key_exists('company2', $fields))
$fields['company2'] = $fields['account2'];
if(array_key_exists('invoice1', $fields))
$fields['surcharge1'] = $fields['invoice1'];
if(array_key_exists('invoice2', $fields))
$fields['surcharge2'] = $fields['invoice2'];
if(array_key_exists('invoice_text1', $fields))
$fields['invoice1'] = $fields['invoice_text1'];
if(array_key_exists('invoice_text2', $fields))
$fields['invoice2'] = $fields['invoice_text2'];
foreach ($fields as &$value) { foreach ($fields as &$value) {
$value = (string) $value; $value = (string) $value;
} }

View File

@ -29,9 +29,7 @@ class GroupSettingRepository extends BaseRepository
$group_setting->save(); $group_setting->save();
} }
nlog($data['settings']); if(!array_key_exists('settings', $data) || count((array)$data['settings']) == 0){
if(count((array)$data['settings']) == 0){
$settings = new \stdClass; $settings = new \stdClass;
$settings->entity = Client::class; $settings->entity = Client::class;
$group_setting->settings = $settings; $group_setting->settings = $settings;

View File

@ -157,9 +157,11 @@ class PaymentRepository extends BaseRepository {
if ( ! $is_existing_payment && ! $this->import_mode ) { if ( ! $is_existing_payment && ! $this->import_mode ) {
if ($payment->client->getSetting('client_manual_payment_notification')) if (array_key_exists('email_receipt', $data) && $data['email_receipt'] == true)
$payment->service()->sendEmail(); $payment->service()->sendEmail();
elseif(!array_key_exists('email_receipt', $data) && $payment->client->getSetting('client_manual_payment_notification'))
$payment->service()->sendEmail();
event( new PaymentWasCreated( $payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null) ) ); event( new PaymentWasCreated( $payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null) ) );
} }

View File

@ -96,8 +96,9 @@ class SubscriptionRepository extends BaseRepository
return $data; return $data;
} }
public function generateLineItems($subscription, $is_recurring = false) public function generateLineItems($subscription, $is_recurring = false, $is_credit = false)
{ {
$multiplier = $is_credit ? -1 : 1;
$line_items = []; $line_items = [];
@ -105,13 +106,13 @@ class SubscriptionRepository extends BaseRepository
{ {
foreach($subscription->service()->products() as $product) foreach($subscription->service()->products() as $product)
{ {
$line_items[] = (array)$this->makeLineItem($product); $line_items[] = (array)$this->makeLineItem($product, $multiplier);
} }
} }
foreach($subscription->service()->recurring_products() as $product) foreach($subscription->service()->recurring_products() as $product)
{ {
$line_items[] = (array)$this->makeLineItem($product); $line_items[] = (array)$this->makeLineItem($product, $multiplier);
} }
$line_items = $this->cleanItems($line_items); $line_items = $this->cleanItems($line_items);
@ -120,13 +121,13 @@ class SubscriptionRepository extends BaseRepository
} }
private function makeLineItem($product) private function makeLineItem($product, $multiplier)
{ {
$item = new InvoiceItem; $item = new InvoiceItem;
$item->quantity = $product->quantity; $item->quantity = $product->quantity;
$item->product_key = $product->product_key; $item->product_key = $product->product_key;
$item->notes = $product->notes; $item->notes = $product->notes;
$item->cost = $product->price; $item->cost = $product->price*$multiplier;
$item->tax_rate1 = $product->tax_rate1 ?: 0; $item->tax_rate1 = $product->tax_rate1 ?: 0;
$item->tax_name1 = $product->tax_name1 ?: ''; $item->tax_name1 = $product->tax_name1 ?: '';
$item->tax_rate2 = $product->tax_rate2 ?: 0; $item->tax_rate2 = $product->tax_rate2 ?: 0;

View File

@ -48,20 +48,24 @@ class ClientService
public function getCreditBalance() :float public function getCreditBalance() :float
{ {
$credits = $this->client->credits $credits = $this->client->credits()
->where('is_deleted', false) ->where('is_deleted', false)
->where('balance', '>', 0) ->where('balance', '>', 0)
->sortBy('created_at'); ->whereDate('due_date', '<=', now()->format('Y-m-d'))
->orWhere('due_date', NULL)
->orderBy('created_at','ASC');
return Number::roundValue($credits->sum('balance'), $this->client->currency()->precision); return Number::roundValue($credits->sum('balance'), $this->client->currency()->precision);
} }
public function getCredits() :Collection public function getCredits() :Collection
{ {
return $this->client->credits return $this->client->credits()
->where('is_deleted', false) ->where('is_deleted', false)
->where('balance', '>', 0) ->where('balance', '>', 0)
->sortBy('created_at'); ->whereDate('due_date', '<=', now()->format('Y-m-d'))
->orWhere('due_date', NULL)
->orderBy('created_at','ASC');
} }
public function getPaymentMethods(float $amount) public function getPaymentMethods(float $amount)

View File

@ -129,7 +129,7 @@ class UpdateReminder extends AbstractService
if ($this->invoice->last_sent_date && if ($this->invoice->last_sent_date &&
(int)$this->settings->endless_reminder_frequency_id > 0) { (int)$this->settings->endless_reminder_frequency_id > 0) {
$reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int)$this->settings->num_days_reminder3)->addSeconds($offset); $reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int)$this->settings->endless_reminder_frequency_id)->addSeconds($offset);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date))); if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date); $date_collection->push($reminder_date);

View File

@ -12,6 +12,7 @@
namespace App\Services\PdfMaker; namespace App\Services\PdfMaker;
use App\Models\Credit;
use App\Models\Quote; use App\Models\Quote;
use App\Services\PdfMaker\Designs\Utilities\BaseDesign; use App\Services\PdfMaker\Designs\Utilities\BaseDesign;
use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
@ -194,6 +195,10 @@ class Design extends BaseDesign
$variables = $this->context['pdf_variables']['quote_details']; $variables = $this->context['pdf_variables']['quote_details'];
} }
if ($this->entity instanceof Credit) {
$variables = $this->context['pdf_variables']['credit_details'];
}
$elements = []; $elements = [];
// We don't want to show account balance or invoice total on PDF.. or any amount with currency. // We don't want to show account balance or invoice total on PDF.. or any amount with currency.

View File

@ -20,6 +20,7 @@ use App\Jobs\Util\SubscriptionWebhookHandler;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\Models\Product; use App\Models\Product;
@ -212,13 +213,23 @@ class SubscriptionService
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->first(); ->first();
//sometimes the last document could be a credit if the user is paying for their service with credits.
if(!$outstanding_invoice){
$outstanding_invoice = Credit::where('subscription_id', $this->subscription->id)
->where('client_id', $recurring_invoice->client_id)
->where('is_deleted', 0)
->orderBy('id', 'desc')
->first();
}
if ($outstanding->count() == 0){ if ($outstanding->count() == 0){
//nothing outstanding //nothing outstanding
return $target->price - $this->calculateProRataRefund($outstanding_invoice); return $target->price - $this->calculateProRataRefundForSubscription($outstanding_invoice);
} }
elseif ($outstanding->count() == 1){ elseif ($outstanding->count() == 1){
//user has multiple amounts outstanding //user has multiple amounts outstanding
return $target->price - $this->calculateProRataRefund($outstanding_invoice); return $target->price - $this->calculateProRataRefundForSubscription($outstanding_invoice);
} }
elseif ($outstanding->count() > 1) { elseif ($outstanding->count() > 1) {
//user is changing plan mid frequency cycle //user is changing plan mid frequency cycle
@ -230,6 +241,35 @@ class SubscriptionService
} }
/**
* We refund unused days left.
*
* @param Invoice $invoice
* @return float
*/
private function calculateProRataRefundForSubscription($invoice) :float
{
if(!$invoice || !$invoice->date)
return 0;
$start_date = Carbon::parse($invoice->date);
$current_date = now();
$days_of_subscription_used = $start_date->diffInDays($current_date);
$days_in_frequency = $this->getDaysInFrequency();
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $this->subscription->price ,2);
// nlog("days in frequency = {$days_in_frequency} - days of subscription used {$days_of_subscription_used}");
// nlog("invoice amount = {$invoice->amount}");
// nlog("pro rata refund = {$pro_rata_refund}");
return $pro_rata_refund;
}
/** /**
* We refund unused days left. * We refund unused days left.
* *
@ -238,7 +278,7 @@ class SubscriptionService
*/ */
private function calculateProRataRefund($invoice) :float private function calculateProRataRefund($invoice) :float
{ {
if(!$invoice->date) if(!$invoice || !$invoice->date)
return 0; return 0;
$start_date = Carbon::parse($invoice->date); $start_date = Carbon::parse($invoice->date);
@ -251,6 +291,10 @@ class SubscriptionService
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2); $pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2);
// nlog("days in frequency = {$days_in_frequency} - days of subscription used {$days_of_subscription_used}");
// nlog("invoice amount = {$invoice->amount}");
// nlog("pro rata refund = {$pro_rata_refund}");
return $pro_rata_refund; return $pro_rata_refund;
} }
@ -265,6 +309,9 @@ class SubscriptionService
*/ */
private function calculateProRataRefundItems($invoice, $is_credit = false) :array private function calculateProRataRefundItems($invoice, $is_credit = false) :array
{ {
if(!$invoice)
return [];
/* depending on whether we are creating an invoice or a credit*/ /* depending on whether we are creating an invoice or a credit*/
$multiplier = $is_credit ? 1 : -1; $multiplier = $is_credit ? 1 : -1;
@ -384,6 +431,46 @@ class SubscriptionService
} }
public function changePlanPaymentCheck($data)
{
$recurring_invoice = $data['recurring_invoice'];
$old_subscription = $data['subscription'];
$target_subscription = $data['target'];
$pro_rata_charge_amount = 0;
$pro_rata_refund_amount = 0;
$last_invoice = Invoice::where('subscription_id', $recurring_invoice->subscription_id)
->where('client_id', $recurring_invoice->client_id)
->where('is_deleted', 0)
->withTrashed()
->orderBy('id', 'desc')
->first();
if(!$last_invoice)
return true;
if($last_invoice->balance > 0)
{
$pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription);
nlog("pro rata charge = {$pro_rata_charge_amount}");
}
else
{
$pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1;
nlog("pro rata refund = {$pro_rata_refund_amount}");
}
$total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price;
if($total_payable > 0)
return true;
return false;
}
/** /**
* When changing plans, we need to generate a pro rata invoice * When changing plans, we need to generate a pro rata invoice
* *
@ -392,6 +479,7 @@ class SubscriptionService
*/ */
public function createChangePlanInvoice($data) public function createChangePlanInvoice($data)
{ {
$recurring_invoice = $data['recurring_invoice']; $recurring_invoice = $data['recurring_invoice'];
$old_subscription = $data['subscription']; $old_subscription = $data['subscription'];
$target_subscription = $data['target']; $target_subscription = $data['target'];
@ -406,7 +494,10 @@ class SubscriptionService
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->first(); ->first();
if($last_invoice->balance > 0) if(!$last_invoice){
//do nothing
}
else if($last_invoice->balance > 0)
{ {
$pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription);
nlog("pro rata charge = {$pro_rata_charge_amount}"); nlog("pro rata charge = {$pro_rata_charge_amount}");
@ -431,7 +522,7 @@ class SubscriptionService
*/ */
private function handlePlanChange($payment_hash) private function handlePlanChange($payment_hash)
{ {
nlog("handle plan change");
$old_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice); $old_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice);
$recurring_invoice = $this->createNewRecurringInvoice($old_recurring_invoice); $recurring_invoice = $this->createNewRecurringInvoice($old_recurring_invoice);
@ -500,7 +591,7 @@ class SubscriptionService
$credit->date = now()->format('Y-m-d'); $credit->date = now()->format('Y-m-d');
$credit->subscription_id = $this->subscription->id; $credit->subscription_id = $this->subscription->id;
$line_items = $subscription_repo->generateLineItems($target); $line_items = $subscription_repo->generateLineItems($target, false, true);
$credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, true)); $credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, true));

View File

@ -36,6 +36,7 @@ class ActivityTransformer extends EntityTransformer
'id' => (string) $this->encodePrimaryKey($activity->id), 'id' => (string) $this->encodePrimaryKey($activity->id),
'activity_type_id' => (string) $activity->activity_type_id, 'activity_type_id' => (string) $activity->activity_type_id,
'client_id' => $activity->client_id ? (string) $this->encodePrimaryKey($activity->client_id) : '', 'client_id' => $activity->client_id ? (string) $this->encodePrimaryKey($activity->client_id) : '',
'recurring_invoice_id' => $activity->recurring_invoice_id ? (string) $this->encodePrimaryKey($activity->recurring_invoice_id) : '',
'company_id' => $activity->company_id ? (string) $this->encodePrimaryKey($activity->company_id) : '', 'company_id' => $activity->company_id ? (string) $this->encodePrimaryKey($activity->company_id) : '',
'user_id' => (string) $this->encodePrimaryKey($activity->user_id), 'user_id' => (string) $this->encodePrimaryKey($activity->user_id),
'invoice_id' => $activity->invoice_id ? (string) $this->encodePrimaryKey($activity->invoice_id) : '', 'invoice_id' => $activity->invoice_id ? (string) $this->encodePrimaryKey($activity->invoice_id) : '',

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.2.7', 'app_version' => '5.2.8',
'app_tag' => '5.2.7-release', 'app_tag' => '5.2.8',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),

File diff suppressed because one or more lines are too long

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -3,36 +3,36 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache'; const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache'; const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = { const RESOURCES = {
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35", "favicon.png": "dca91c54388f52eded692718d5a98b8b",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed", "main.dart.js": "ec0d23274ffa0ea4b6743fe88c16ccaa",
"version.json": "9fe5b22a16f39b766c8fdc35a24b3efa", "/": "23224b5e03519aaa87594403d54412cf",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3", "manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
"assets/AssetManifest.json": "7e49562f32e24a9e2557fe4178a84b79",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296", "assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"assets/NOTICES": "687b68d41e137cfbdee105c0b9be3e9d",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac", "assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3", "assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541", "assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6", "assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868", "assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08", "assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978", "assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5", "assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024", "assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71", "assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629", "assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/NOTICES": "687b68d41e137cfbdee105c0b9be3e9d", "assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f", "assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"favicon.png": "dca91c54388f52eded692718d5a98b8b", "assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"main.dart.js": "383f48eff49849cbbe38e2fe4ae81ad4", "assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"/": "23224b5e03519aaa87594403d54412cf" "assets/AssetManifest.json": "7e49562f32e24a9e2557fe4178a84b79",
"version.json": "4d10e2258012cbb88b24009334a24f24",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"favicon.ico": "51636d3a390451561744c42188ccd628"
}; };
// The application shell files that are downloaded before a service worker can // The application shell files that are downloaded before a service worker can

221260
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

219586
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5", "/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=d9b987796d537e68bee7", "/css/app.css": "/css/app.css?id=9d6698418e6cdd571d49",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4", "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1", "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7", "/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
@ -21,5 +21,7 @@
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=5c35d28cf0a3286e7c45", "/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=5c35d28cf0a3286e7c45",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=fc3055d6a099f523ea98", "/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=fc3055d6a099f523ea98",
"/js/setup/setup.js": "/js/setup/setup.js?id=8d454e7090f119552a6c", "/js/setup/setup.js": "/js/setup/setup.js?id=8d454e7090f119552a6c",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad" "/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad",
"/js/admin.js": "/js/admin.js?id=003930085af69b13a86a",
"/css/admin.css": "/css/admin.css?id=0ca9742f0da64aa54214"
} }

View File

@ -1 +1 @@
{"app_name":"invoiceninja_flutter","version":"5.0.51","build_number":"51"} {"app_name":"invoiceninja_flutter","version":"5.0.52","build_number":"52"}

View File

@ -4272,6 +4272,7 @@ $LANG = array(
'default_payment_method' => 'Make this your preferred way of paying.', 'default_payment_method' => 'Make this your preferred way of paying.',
'already_default_payment_method' => 'This is your preferred way of paying.', 'already_default_payment_method' => 'This is your preferred way of paying.',
'auto_bill_disabled' => 'Auto Bill Disabled', 'auto_bill_disabled' => 'Auto Bill Disabled',
'select_payment_method' => 'Select a payment method:',
); );
return $LANG; return $LANG;

View File

@ -1,4 +1,5 @@
<a class="button-link" x-on:click="{{ $property }} = true" href="#">{{ ctrans("texts.$property") }}</a> <a class="button-link underline" x-on:click="{{ $property }} = true" href="#">{{ ctrans("texts.$property") }}</a>
<span class="text-gray-300">/</span> <span class="text-gray-300">/</span>
<div x-show="{{ $property }}" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center"> <div x-show="{{ $property }}" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
@ -20,11 +21,11 @@
{{ $title }} {{ $title }}
</h3> </h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm leading-5 text-gray-500"> <p class="text-sm leading-5 text-gray-500 h-64 overflow-y-scroll">
{{ $content }} {{ $content }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,14 +15,19 @@
alt="Background image"> alt="Background image">
</div> </div>
@endif @endif
<div class="col-span-2 h-screen flex"> <div class="col-span-2 h-screen flex">
<div class="m-auto md:w-1/2 lg:w-1/4"> <div class="m-auto md:w-1/2 lg:w-1/4">
@if($account && !$account->isPaid()) @if($account && !$account->isPaid())
<div> <div>
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}" <img src="{{ asset('images/invoiceninja-black-logo-2.png') }}"
class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo"> class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div> </div>
@elseif(isset($company) && !is_null($company))
<div>
<img src="{{ $company->present()->logo() }}"
class="border-b border-gray-100 h-18 pb-4" alt="{{ $company->present()->name() }} logo">
</div>
@endif @endif
<div class="flex flex-col"> <div class="flex flex-col">

View File

@ -8,7 +8,6 @@
<img class="h-32 w-auto" src="{{ $company->present()->logo() }}" alt="{{ ctrans('texts.logo') }}"> <img class="h-32 w-auto" src="{{ $company->present()->logo() }}" alt="{{ ctrans('texts.logo') }}">
</div> </div>
<h1 class="text-center text-3xl mt-8">{{ ctrans('texts.register') }}</h1> <h1 class="text-center text-3xl mt-8">{{ ctrans('texts.register') }}</h1>
<h1 class="text-center text-3xl mt-8">{{ ctrans('texts.register') }}</h1>
<p class="block text-center text-gray-600">{{ ctrans('texts.register_label') }}</p> <p class="block text-center text-gray-600">{{ ctrans('texts.register_label') }}</p>
<form action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{ more: false }"> <form action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{ more: false }">
@ -29,11 +28,15 @@
<input type="checkbox" name="terms" class="form-checkbox mr-2 cursor-pointer" checked> <input type="checkbox" name="terms" class="form-checkbox mr-2 cursor-pointer" checked>
<span class="text-sm text-gray-800"> <span class="text-sm text-gray-800">
{{ ctrans('texts.i_agree') }} {{ ctrans('texts.i_agree_to_the') }}
@endif @endif
@includeWhen(!empty($company->settings->client_portal_terms), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'terms_of_service', 'title' => ctrans('texts.terms_of_service'), 'content' => $company->settings->client_portal_terms]) @includeWhen(!empty($company->settings->client_portal_terms), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'terms_of_service', 'title' => ctrans('texts.terms_of_service'), 'content' => $company->settings->client_portal_terms])
@includeWhen(!empty($company->settings->client_portal_privacy_policy), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'privacy_policy', 'title' => ctrans('texts.privacy_policy'), 'content' => $company->settings->client_portal_privacy_policy]) @includeWhen(!empty($company->settings->client_portal_privacy_policy), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'privacy_policy', 'title' => ctrans('texts.privacy_policy'), 'content' => $company->settings->client_portal_privacy_policy])
@error('terms')
<p class="text-red-600">{{ $message }}</p>
@enderror
</span> </span>
</span> </span>

View File

@ -0,0 +1,29 @@
@if($entity->documents->count() > 0)
<div class="bg-white shadow sm:rounded-lg my-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<p class="text-lg leading-6 font-medium text-gray-900">{{ ctrans('texts.attachments') }}:</p>
@foreach($entity->documents as $document)
<div class="inline-flex items-center space-x-1">
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block text-sm button-link text-primary">{{ Illuminate\Support\Str::limit($document->name, 40) }}</a>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
@if(!$loop->last)
<span>&mdash;</span>
@endif
</div>
@endforeach
</div>
</div>
</div>
</div>
@endif

View File

@ -10,11 +10,10 @@
</div> </div>
<div class="relative flex justify-center text-sm leading-5"> <div class="relative flex justify-center text-sm leading-5">
<span class="font-bold tracking-wide bg-gray-100 px-6 py-0">Select a payment method:</span> <span class="font-bold tracking-wide bg-gray-100 px-6 py-0">{{ ctrans('texts.select_payment_method')}}</span>
{{-- <h1 class="text-2xl font-bold tracking-wide bg-gray-100 px-6 py-0">--}} <h1 class="text-2xl font-bold tracking-wide bg-gray-100 px-6 py-0">
{{-- {{ ctrans('texts.total') }}: {{ \App\Utils\Number::formatMoney($total, $subscription->company) }}--}} {{ ctrans('texts.total') }}: {{ \App\Utils\Number::formatMoney($amount, $subscription->company) }}
{{-- <small class="ml-1 line-through text-gray-500">{{ \App\Utils\Number::formatMoney($subscription->price, $subscription->company) }}</small>--}} </h1>
{{-- </h1>--}}
</div> </div>
</div> </div>
@ -63,9 +62,19 @@
</div> </div>
</div> </div>
@elseif($amount < 0) @elseif($amount < 0)
<button wire:click="handlePaymentNotRequired"class="px-3 py-2 border rounded mr-4 hover:border-blue-600"> <div class="relative flex justify-center text-sm leading-5">
<h1 class="text-2xl font-bold tracking-wide bg-gray-100 px-6 py-0">
{{ ctrans('texts.total') }}: {{ \App\Utils\Number::formatMoney($amount, $subscription->company) }}
</h1>
</div>
<div class="relative flex justify-center text-sm leading-5 mt-10">
<button wire:click="handlePaymentNotRequired" class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
{{ ctrans('texts.click_to_continue') }} {{ ctrans('texts.click_to_continue') }}
</button> </button>
</div>
@endif @endif
</div> </div>
</div> </div>

View File

@ -40,6 +40,8 @@
{{ ctrans('texts.date') }} {{ ctrans('texts.date') }}
</p> </p>
</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -63,6 +65,12 @@
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"> <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $recurring_invoice->formatDate($recurring_invoice->date, $recurring_invoice->client->date_format()) }} {{ $recurring_invoice->formatDate($recurring_invoice->date, $recurring_invoice->client->date_format()) }}
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
<a href="{{ route('client.recurring_invoice.show', $recurring_invoice->hashed_id) }}"
class="button-link text-primary">
{{ ctrans('texts.view') }}
</a>
</td>
</tr> </tr>
@empty @empty
<tr class="bg-white group hover:bg-gray-100"> <tr class="bg-white group hover:bg-gray-100">

View File

@ -7,6 +7,8 @@
@endpush @endpush
@section('body') @section('body')
@include('portal.ninja2020.components.entity-documents', ['entity' => $credit])
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<section class="flex items-center"> <section class="flex items-center">
<div class="items-center" style="display: none" id="pagination-button-container"> <div class="items-center" style="display: none" id="pagination-button-container">

View File

@ -53,5 +53,14 @@
document.getElementById('pay-now').addEventListener('click', function () { document.getElementById('pay-now').addEventListener('click', function () {
document.getElementById('server-response').submit(); document.getElementById('server-response').submit();
}); });
document.addEventListener('DOMContentLoaded', () => {
let firstAccount = document
.querySelector('.toggle-payment-with-token')
firstAccount.checked = true;
document.querySelector('input[name=source]').value = firstAccount.dataset.token;
});
</script> </script>
@endpush @endpush

View File

@ -71,5 +71,13 @@
@endsection @endsection
@section('gateway_footer') @section('gateway_footer')
<script>
Livewire.on('passed-required-fields-check', (event) => {
if (event.hasOwnProperty('client_postal_code')) {
document.querySelector('meta[name=client-postal-code]').content = event.client_postal_code;
}
});
</script>
<script src="{{ asset('js/clients/payments/wepay-credit-card.js') }}"></script> <script src="{{ asset('js/clients/payments/wepay-credit-card.js') }}"></script>
@endsection @endsection

View File

@ -9,7 +9,7 @@
<h3 class="text-xl leading-6 font-medium text-gray-900"> <h3 class="text-xl leading-6 font-medium text-gray-900">
{{ ctrans('texts.terms') }} {{ ctrans('texts.terms') }}
</h3> </h3>
<div class="mt-4"> <div class="mt-4 h-64 overflow-y-scroll">
@foreach($entities as $entity) @foreach($entities as $entity)
<div class="mb-4"> <div class="mb-4">
<p class="text-sm leading-6 font-medium text-gray-500">{{ $entity_type }} {{ $entity->number }}:</p> <p class="text-sm leading-6 font-medium text-gray-500">{{ $entity_type }} {{ $entity->number }}:</p>

View File

@ -70,35 +70,7 @@
</div> </div>
@endif @endif
@if($invoice->documents->count() > 0) @include('portal.ninja2020.components.entity-documents', ['entity' => $invoice])
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<p class="text-lg leading-6 font-medium text-gray-900">{{ ctrans('texts.attachments') }}:</p>
@foreach($invoice->documents as $document)
<div class="inline-flex items-center space-x-1">
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block text-sm button-link text-primary">{{ Illuminate\Support\Str::limit($document->name, 40) }}</a>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
@if(!$loop->last)
<span>&mdash;</span>
@endif
</div>
@endforeach
</div>
</div>
</div>
</div>
@endif
<div class="flex items-center justify-between mt-4"> <div class="flex items-center justify-between mt-4">
<section class="flex items-center"> <section class="flex items-center">

View File

@ -25,15 +25,19 @@
</div> </div>
@endif @endif
@include('portal.ninja2020.components.entity-documents', ['entity' => $quote])
<div class="flex items-center justify-between mt-4"> <div class="flex items-center justify-between mt-4">
<section class="flex items-center"> <section class="flex items-center">
<div class="items-center" style="display: none" id="pagination-button-container"> <div class="items-center" style="display: none" id="pagination-button-container">
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300" id="previous-page-button" title="Previous page"> <button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300"
id="previous-page-button" title="Previous page">
<svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg> </svg>
</button> </button>
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300" id="next-page-button" title="Next page"> <button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300"
id="next-page-button" title="Next page">
<svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg> </svg>

View File

@ -59,12 +59,15 @@
</div> </div>
</div> </div>
@include('portal.ninja2020.components.entity-documents', ['entity' => $invoice])
@if($invoice->auto_bill === 'optin' || $invoice->auto_bill === 'optout') @if($invoice->auto_bill === 'optin' || $invoice->auto_bill === 'optout')
<div class="bg-white shadow overflow-hidden lg:rounded-lg mt-4"> <div class="bg-white shadow overflow-hidden lg:rounded-lg mt-4">
<div class="flex flex-col md:flex-row items-start justify-between px-4 py-5 sm:p-6"> <div class="flex flex-col md:flex-row items-start justify-between px-4 py-5 sm:p-6">
<div> <div>
<h3 class="text-lg leading-6 font-medium text-gray-900">Auto Bill</h3> <h3 class="text-lg leading-6 font-medium text-gray-900">Auto Bill</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">Change your update bill preferences.</p> <p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">Change your update bill
preferences.</p>
</div> </div>
<div class="flex mt-4 space-x-2"> <div class="flex mt-4 space-x-2">

View File

@ -23,7 +23,7 @@ Route::get('tmp_pdf/{hash}', 'ClientPortal\TempRouteController@index')->name('tm
Route::get('client/key_login/{contact_key}', 'ClientPortal\ContactHashLoginController@login')->name('client.contact_login')->middleware(['domain_db','contact_key_login']); Route::get('client/key_login/{contact_key}', 'ClientPortal\ContactHashLoginController@login')->name('client.contact_login')->middleware(['domain_db','contact_key_login']);
Route::get('client/magic_link/{magic_link}', 'ClientPortal\ContactHashLoginController@magicLink')->name('client.contact_magic_link')->middleware(['domain_db','contact_key_login']); Route::get('client/magic_link/{magic_link}', 'ClientPortal\ContactHashLoginController@magicLink')->name('client.contact_magic_link')->middleware(['domain_db','contact_key_login']);
Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download'); Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download')->middleware(['document_db']);
Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name('client.error'); Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name('client.error');
Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () { Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () {

View File

@ -0,0 +1,143 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\ClientPortal;
use App\Http\Livewire\CreditsTable;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Credit;
use App\Models\User;
use Faker\Factory;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire;
use Tests\TestCase;
class CreditsTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->faker = Factory::create();
}
public function testShowingOnlyCreditsWithDueDateLessOrEqualToNow()
{
$account = Account::factory()->create();
$user = User::factory()->create(
['account_id' => $account->id, 'email' => $this->faker->safeEmail]
);
$company = Company::factory()->create(['account_id' => $account->id]);
$client = Client::factory()->create(['company_id' => $company->id, 'user_id' => $user->id]);
ClientContact::factory()->count(2)->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-01',
'due_date' => now()->subDays(5),
'status_id' => Credit::STATUS_SENT,
]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-02',
'due_date' => now(),
'status_id' => Credit::STATUS_SENT,
]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-03',
'due_date' => now()->addDays(5),
'status_id' => Credit::STATUS_SENT,
]);
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company])
->assertSee('testing-number-01')
->assertSee('testing-number-02')
->assertDontSee('testing-number-03');
}
public function testShowingCreditsWithNullDueDate()
{
$account = Account::factory()->create();
$user = User::factory()->create(
['account_id' => $account->id, 'email' => $this->faker->safeEmail]
);
$company = Company::factory()->create(['account_id' => $account->id]);
$client = Client::factory()->create(['company_id' => $company->id, 'user_id' => $user->id]);
ClientContact::factory()->count(2)->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-01',
'status_id' => Credit::STATUS_SENT,
]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-02',
'due_date' => now(),
'status_id' => Credit::STATUS_SENT,
]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-03',
'due_date' => now()->addDays(5),
'status_id' => Credit::STATUS_SENT,
]);
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company])
->assertSee('testing-number-01')
->assertSee('testing-number-02')
->assertDontSee('testing-number-03');
}
}

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