diff --git a/app/Http/Controllers/Auth/ContactRegisterController.php b/app/Http/Controllers/Auth/ContactRegisterController.php index ef4fe9502c89..0fd09ecbc2f8 100644 --- a/app/Http/Controllers/Auth/ContactRegisterController.php +++ b/app/Http/Controllers/Auth/ContactRegisterController.php @@ -29,7 +29,7 @@ class ContactRegisterController extends Controller public function showRegisterForm(string $company_key = '') { - $key = request()->session()->has('key') ? request()->session()->get('key') : $company_key; + $key = request()->session()->has('company_key') ? request()->session()->get('company_key') : $company_key; $company = Company::where('company_key', $key)->firstOrFail(); diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index c3c752e29e0e..84e305021579 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -89,6 +89,12 @@ class InvoiceController extends Controller * @param ProcessInvoicesInBulkRequest $request * @return mixed */ + + public function catch_bulk() + { + return $this->render('invoices.index'); + } + public function bulk(ProcessInvoicesInBulkRequest $request) { $transformed_ids = $this->transformKeys($request->invoices); diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index f5cb923d5d68..7bdb1c2dbdc0 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -69,6 +69,11 @@ class PaymentController extends Controller ]); } + public function catch_process(Request $request) + { + return $this->render('payments.index'); + } + /** * Presents the payment screen for a given * gateway and payment method. diff --git a/app/Http/Controllers/ClientPortal/QuoteController.php b/app/Http/Controllers/ClientPortal/QuoteController.php index 8b338eb49ed2..c646d41efee9 100644 --- a/app/Http/Controllers/ClientPortal/QuoteController.php +++ b/app/Http/Controllers/ClientPortal/QuoteController.php @@ -30,6 +30,7 @@ use Illuminate\View\View; use Symfony\Component\HttpFoundation\BinaryFileResponse; use ZipStream\Option\Archive; use ZipStream\ZipStream; +use Illuminate\Http\Request; class QuoteController extends Controller { @@ -58,17 +59,16 @@ class QuoteController extends Controller 'quote' => $quote, ]; + $invitation = $quote->invitations()->where('client_contact_id', auth()->user()->id)->first(); - $invitation = $quote->invitations()->where('client_contact_id', auth()->user()->id)->first(); + if ($invitation && auth()->guard('contact') && ! request()->has('silent') && ! $invitation->viewed_date) { - if ($invitation && auth()->guard('contact') && ! request()->has('silent') && ! $invitation->viewed_date) { + $invitation->markViewed(); - $invitation->markViewed(); - - event(new InvitationWasViewed($quote, $invitation, $quote->company, Ninja::eventVars())); - event(new QuoteWasViewed($invitation, $invitation->company, Ninja::eventVars())); - - } + event(new InvitationWasViewed($quote, $invitation, $quote->company, Ninja::eventVars())); + event(new QuoteWasViewed($invitation, $invitation->company, Ninja::eventVars())); + + } if ($request->query('mode') === 'fullscreen') { return render('quotes.show-fullscreen', $data); @@ -82,7 +82,7 @@ class QuoteController extends Controller $transformed_ids = $this->transformKeys($request->quotes); if ($request->action == 'download') { - return $this->downloadQuotePdf((array) $transformed_ids); + return $this->downloadQuotes((array) $transformed_ids); } if ($request->action = 'approve') { @@ -92,10 +92,32 @@ class QuoteController extends Controller return back(); } + public function downloadQuotes($ids) + { + + $data['quotes'] = Quote::whereIn('id', $ids) + ->whereClientId(auth()->user()->client->id) + ->withTrashed() + ->get(); + + if(count($data['quotes']) == 0) + return back()->with(['message' => ctrans('texts.no_items_selected')]); + + return $this->render('quotes.download', $data); + } + + public function download(Request $request) + { + $transformed_ids = $this->transformKeys($request->quotes); + + return $this->downloadQuotePdf((array) $transformed_ids); + } + protected function downloadQuotePdf(array $ids) { $quotes = Quote::whereIn('id', $ids) ->whereClientId(auth()->user()->client->id) + ->withTrashed() ->get(); if (! $quotes || $quotes->count() == 0) { @@ -136,6 +158,7 @@ class QuoteController extends Controller ->where('client_id', auth('contact')->user()->client->id) ->where('company_id', auth('contact')->user()->client->company_id) ->where('status_id', Quote::STATUS_SENT) + ->withTrashed() ->get(); if (!$quotes || $quotes->count() == 0) { diff --git a/app/Http/Middleware/ContactRegister.php b/app/Http/Middleware/ContactRegister.php index 6c8922ff65ba..9ec5805a3247 100644 --- a/app/Http/Middleware/ContactRegister.php +++ b/app/Http/Middleware/ContactRegister.php @@ -37,7 +37,7 @@ class ContactRegister if(! $company->client_can_register) abort(400, 'Registration disabled'); - session()->put('key', $company->company_key); + session()->put('company_key', $company->company_key); return $next($request); } @@ -56,7 +56,7 @@ class ContactRegister abort(400, 'Registration disabled'); // $request->merge(['key' => $company->company_key]); - session()->put('key', $company->company_key); + session()->put('company_key', $company->company_key); return $next($request); } @@ -71,7 +71,7 @@ class ContactRegister abort(400, 'Registration disabled'); //$request->merge(['key' => $company->company_key]); - session()->put('key', $company->company_key); + session()->put('company_key', $company->company_key); return $next($request); } @@ -85,7 +85,7 @@ class ContactRegister abort(400, 'Registration disabled'); //$request->merge(['key' => $company->company_key]); - session()->put('key', $company->company_key); + session()->put('company_key', $company->company_key); return $next($request); } diff --git a/app/Http/Requests/Gateways/GoCardless/IbpRequest.php b/app/Http/Requests/Gateways/GoCardless/IbpRequest.php index 8caeaf64ca2b..c66d3afeefa5 100644 --- a/app/Http/Requests/Gateways/GoCardless/IbpRequest.php +++ b/app/Http/Requests/Gateways/GoCardless/IbpRequest.php @@ -18,6 +18,7 @@ use App\Models\CompanyGateway; use App\Models\PaymentHash; use App\Utils\Traits\MakesHash; use Illuminate\Foundation\Http\FormRequest; +use App\Libraries\MultiDB; class IbpRequest extends FormRequest { @@ -30,6 +31,8 @@ class IbpRequest extends FormRequest */ public function authorize() { + MultiDB::findAndSetDbByCompanyKey($this->company_key); + return true; } diff --git a/app/Http/Requests/Payments/PaymentWebhookRequest.php b/app/Http/Requests/Payments/PaymentWebhookRequest.php index 5aac0825e4f4..ef805d1367bd 100644 --- a/app/Http/Requests/Payments/PaymentWebhookRequest.php +++ b/app/Http/Requests/Payments/PaymentWebhookRequest.php @@ -47,7 +47,7 @@ class PaymentWebhookRequest extends Request */ public function getCompanyGateway() { - return CompanyGateway::findOrFail($this->decodePrimaryKey($this->company_gateway_id)); + return CompanyGateway::withTrashed()->findOrFail($this->decodePrimaryKey($this->company_gateway_id)); } /** diff --git a/app/Import/Transformers/Csv/InvoiceTransformer.php b/app/Import/Transformers/Csv/InvoiceTransformer.php index e6f963a2dd1d..dfd6d8de2e2c 100644 --- a/app/Import/Transformers/Csv/InvoiceTransformer.php +++ b/app/Import/Transformers/Csv/InvoiceTransformer.php @@ -126,8 +126,6 @@ class InvoiceTransformer extends BaseTransformer { } $transformed['line_items'] = $line_items; -nlog($transformed); - return $transformed; } } diff --git a/app/Jobs/Ninja/SendReminders.php b/app/Jobs/Ninja/SendReminders.php index cba282e06d27..20aa17021ef5 100644 --- a/app/Jobs/Ninja/SendReminders.php +++ b/app/Jobs/Ninja/SendReminders.php @@ -28,6 +28,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\App; //@DEPRECATED class SendReminders implements ShouldQueue @@ -284,6 +285,11 @@ class SendReminders implements ShouldQueue */ private function setLateFee($invoice, $amount, $percent) :Invoice { + App::forgetInstance('translator'); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($invoice->client->getMergedSettings())); + App::setLocale($invoice->client->locale()); + $temp_invoice_balance = $invoice->balance; if ($amount <= 0 && $percent <= 0) { @@ -313,8 +319,8 @@ class SendReminders implements ShouldQueue /**Refresh Invoice values*/ $invoice = $invoice->calc()->getInvoice(); - $this->invoice->client->service()->updateBalance($this->invoice->balance - $temp_invoice_balance)->save(); - $this->invoice->ledger()->updateInvoiceBalance($this->invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$this->invoice->number}"); + $invoice->client->service()->updateBalance($invoice->balance - $temp_invoice_balance)->save(); + $invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}"); return $invoice; } diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 0f722256a3c1..4d992607a054 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -83,8 +83,6 @@ class SendRecurring implements ShouldQueue //->createInvitations() //need to only link invitations to those in the recurring invoice ->fillDefaults() ->save(); - - $invoice = $this->createRecurringInvitations($invoice); } else{ @@ -94,6 +92,8 @@ class SendRecurring implements ShouldQueue ->save(); } + $invoice = $this->createRecurringInvitations($invoice); + nlog("updating recurring invoice dates"); /* Set next date here to prevent a recurring loop forming */ $this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate(); @@ -122,27 +122,30 @@ class SendRecurring implements ShouldQueue event('eloquent.created: App\Models\Invoice', $invoice); - //Admin notification for recurring invoice sent. - if ($invoice->invitations->count() >= 1 ) { - $invoice->entityEmailEvent($invoice->invitations->first(), 'invoice', 'email_template_invoice'); - } - - nlog("Invoice {$invoice->number} created"); - - $invoice->invitations->each(function ($invitation) use ($invoice) { - if ($invitation->contact && !$invitation->contact->trashed() && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) { - - try{ - EmailEntity::dispatch($invitation, $invoice->company)->delay(now()->addSeconds(1)); - } - catch(\Exception $e) { - nlog($e->getMessage()); - } - - nlog("Firing email for invoice {$invoice->number}"); + if($invoice->client->getSetting('auto_email_invoice')) + { + //Admin notification for recurring invoice sent. + if ($invoice->invitations->count() >= 1 ) { + $invoice->entityEmailEvent($invoice->invitations->first(), 'invoice', 'email_template_invoice'); } - }); - + + nlog("Invoice {$invoice->number} created"); + + $invoice->invitations->each(function ($invitation) use ($invoice) { + if ($invitation->contact && !$invitation->contact->trashed() && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) { + + try{ + EmailEntity::dispatch($invitation, $invoice->company)->delay(now()->addSeconds(1)); + } + catch(\Exception $e) { + nlog($e->getMessage()); + } + + nlog("Firing email for invoice {$invoice->number}"); + } + }); + } + if ($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $invoice->auto_bill_enabled) { nlog("attempting to autobill {$invoice->number}"); $invoice->service()->autoBill(); diff --git a/app/Jobs/Util/ReminderJob.php b/app/Jobs/Util/ReminderJob.php index 55b0ae690703..947d12cccf4c 100644 --- a/app/Jobs/Util/ReminderJob.php +++ b/app/Jobs/Util/ReminderJob.php @@ -152,9 +152,11 @@ class ReminderJob implements ShouldQueue */ private function setLateFee($invoice, $amount, $percent) :Invoice { + App::forgetInstance('translator'); $t = app('translator'); $t->replace(Ninja::transformTranslations($invoice->client->getMergedSettings())); + App::setLocale($invoice->client->locale()); $temp_invoice_balance = $invoice->balance; diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 9d6d31e1d09c..8858e0133284 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -222,17 +222,6 @@ class BaseRepository if (array_key_exists('documents', $data)) $this->saveDocuments($data['documents'], $model); - /* Marks whether the client contact should receive emails based on the send_email property */ - // if (isset($data['client_contacts'])) { - // foreach ($data['client_contacts'] as $contact) { - // if ($contact['send_email'] == 1 && is_string($contact['id'])) { - // $client_contact = ClientContact::find($this->decodePrimaryKey($contact['id'])); - // $client_contact->send_email = true; - // $client_contact->save(); - // } - // } - // } - /* If invitations are present we need to filter existing invitations with the new ones */ if (isset($data['invitations'])) { $invitations = collect($data['invitations']); diff --git a/app/Services/Invoice/AddGatewayFee.php b/app/Services/Invoice/AddGatewayFee.php index b63f7a43e4d9..d5699234be05 100644 --- a/app/Services/Invoice/AddGatewayFee.php +++ b/app/Services/Invoice/AddGatewayFee.php @@ -77,6 +77,7 @@ class AddGatewayFee extends AbstractService App::forgetInstance('translator'); $t = app('translator'); $t->replace(Ninja::transformTranslations($this->invoice->company->settings)); + App::setLocale($this->invoice->client->locale()); $invoice_item = new InvoiceItem; $invoice_item->type_id = '3'; diff --git a/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index 8416ae729f47..fee18cd16cda 100644 --- a/app/Services/PdfMaker/Design.php +++ b/app/Services/PdfMaker/Design.php @@ -621,12 +621,13 @@ class Design extends BaseDesign $variables = $this->context['pdf_variables']['total_columns']; + /* 'labels' is a protected value - if the user enters labels it attempts to replace this string again - we need to set labels are a protected text label and remove it from the string */ $elements = [ ['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [ - ['element' => 'p', 'content' => strtr($_variables['values']['$entity.public_notes'], $_variables), 'properties' => ['data-ref' => 'total_table-public_notes', 'style' => 'text-align: left;']], + ['element' => 'p', 'content' => strtr(str_replace("labels", "", $_variables['values']['$entity.public_notes']), $_variables), 'properties' => ['data-ref' => 'total_table-public_notes', 'style' => 'text-align: left;']], ['element' => 'p', 'content' => '', 'properties' => ['style' => 'text-align: left; display: flex; flex-direction: column;'], 'elements' => [ ['element' => 'span', 'content' => '$entity.terms_label: ', 'properties' => ['hidden' => $this->entityVariableCheck('$entity.terms'), 'data-ref' => 'total_table-terms-label', 'style' => 'font-weight: bold; text-align: left; margin-top: 1rem;']], - ['element' => 'span', 'content' => strtr($_variables['values']['$entity.terms'], $_variables['labels']), 'properties' => ['data-ref' => 'total_table-terms', 'style' => 'text-align: left;']], + ['element' => 'span', 'content' => strtr(str_replace("labels", "", $_variables['values']['$entity.terms']), $_variables['labels']), 'properties' => ['data-ref' => 'total_table-terms', 'style' => 'text-align: left;']], ]], ['element' => 'img', 'properties' => ['style' => 'max-width: 50%; height: auto;', 'src' => '$contact.signature', 'id' => 'contact-signature']], ['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: flex; align-items: flex-start;'], 'elements' => [ diff --git a/config/ninja.php b/config/ninja.php index af359dbc75f7..5015fcc8a177 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -38,7 +38,7 @@ return [ 'trusted_proxies' => env('TRUSTED_PROXIES', false), 'is_docker' => env('IS_DOCKER', false), 'local_download' => env('LOCAL_DOWNLOAD', false), - 'sentry_dsn' => env('SENTRY_LARAVEL_DSN', 'https://32f01ea994744fa08a0f688769cef78a@sentry.invoicing.co/9'), + 'sentry_dsn' => env('SENTRY_LARAVEL_DSN', 'https://39389664f3f14969b4c43dadda00a40b@sentry2.invoicing.co/5'), 'environment' => env('NINJA_ENVIRONMENT', 'selfhost'), // 'hosted', 'development', 'selfhost', 'reseller' 'preconfigured_install' => env('PRECONFIGURED_INSTALL',false), 'update_secret' => env('UPDATE_SECRET', ''), diff --git a/phpunit.yml b/phpunit.yml new file mode 100644 index 000000000000..d77a3ccffa97 --- /dev/null +++ b/phpunit.yml @@ -0,0 +1,108 @@ +on: + push: + branches: + - v5-develop + pull_request: + branches: + - v5-develop + +name: phpunit +jobs: + run: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-18.04', 'ubuntu-20.04'] + php-versions: ['7.3','7.4','8.0'] + phpunit-versions: ['latest'] + + env: + DB_DATABASE1: ninja + DB_USERNAME1: root + DB_PASSWORD1: ninja + DB_HOST1: '127.0.0.1' + DB_DATABASE: ninja + DB_USERNAME: root + DB_PASSWORD: ninja + DB_HOST: '127.0.0.1' + BROADCAST_DRIVER: log + CACHE_DRIVER: file + QUEUE_CONNECTION: sync + SESSION_DRIVER: file + NINJA_ENVIRONMENT: hosted + MULTI_DB_ENABLED: false + NINJA_LICENSE: 123456 + TRAVIS: true + MAIL_MAILER: log + + services: + mariadb: + image: mariadb:latest + ports: + - 32768:3306 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_USER: ninja + MYSQL_PASSWORD: ninja + MYSQL_DATABASE: ninja + MYSQL_ROOT_PASSWORD: ninja + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Start mysql service + run: | + sudo systemctl start mysql.service + - name: Verify MariaDB connection + env: + DB_PORT: ${{ job.services.mariadb.ports[3306] }} + DB_PORT1: ${{ job.services.mariadb.ports[3306] }} + + run: | + while ! mysqladmin ping -h"127.0.0.1" -P"$DB_PORT" --silent; do + sleep 1 + done + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mysql, mysqlnd, sqlite3, bcmath, gmp, gd, curl, zip, openssl, mbstring, xml + + - uses: actions/checkout@v1 + with: + ref: v5-develop + fetch-depth: 1 + + - name: Copy .env + run: | + cp .env.ci .env + - name: Install composer dependencies + run: | + composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + composer install + - name: Prepare Laravel Application + run: | + php artisan key:generate + php artisan optimize + php artisan cache:clear + php artisan config:cache + - name: Create DB and schemas + run: | + mkdir -p database + touch database/database.sqlite + - name: Migrate Database + run: | + php artisan migrate:fresh --seed --force && php artisan db:seed --force + - name: Prepare JS/CSS assets + run: | + npm i + npm run production + - name: Run Testsuite + run: | + cat .env + vendor/bin/phpunit --testdox + env: + DB_PORT: ${{ job.services.mysql.ports[3306] }} + + - name: Run php-cs-fixer + run: | + vendor/bin/php-cs-fixer fix diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 7e273e7ff56e..9ec92685c9ba 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4340,7 +4340,18 @@ $LANG = array( 'no_available_methods' => 'We can\'t find any credit cards on your device. Read more about this.', 'gocardless_mandate_not_ready' => 'Payment mandate is not ready. Please try again later.', 'payment_type_instant_bank_pay' => 'Instant Bank Pay', - + 'payment_type_iDEAL' => 'iDEAL', + 'payment_type_Przelewy24' => 'Przelewy24', + 'payment_type_Mollie Bank Transfer' => 'Bank Transfer', + 'payment_type_KBC/CBC' => 'KBC/CBC', + 'payment_type_Instant Bank Pay' => 'Instant Bank Pay', + 'payment_type_Hosted Page' => 'Hosted Page', + 'payment_type_GiroPay' => 'GiroPay', + 'payment_type_EPS' => 'EPS', + 'payment_type_Direct Debit' => 'Direct Debit', + 'payment_type_Bancontact' => 'Bancontact', + 'payment_type_BECS' => 'BECS', + 'payment_type_ACSS' => 'ACSS', ); return $LANG; diff --git a/resources/views/pdf-designs/modern.html b/resources/views/pdf-designs/modern.html index 84dcbf1280c9..826dd88a7671 100644 --- a/resources/views/pdf-designs/modern.html +++ b/resources/views/pdf-designs/modern.html @@ -49,8 +49,8 @@ #header, #header-spacer { height: 160px; - padding: 3rem; - margin-bottom: 3rem; + padding: 1rem; + margin-bottom: 1rem; } .company-name { @@ -73,7 +73,7 @@ } .logo-client-wrapper { - margin: 0 2rem 3rem; + margin: 0 2rem 0rem; display: grid; grid-template-columns: 1.5fr 1fr; } @@ -92,7 +92,7 @@ } .table-wrapper { - margin: 3rem 2rem; + margin: 0rem 2rem; } [data-ref="table"] { @@ -149,8 +149,8 @@ #footer, #footer-spacer { height: 220px; - padding: 1rem 1.5rem; - margin-top: 1rem; + padding: 0rem 0rem; + margin-top: 0rem; } .footer-content { @@ -161,11 +161,13 @@ color: #fff4e9; max-height: 140px; justify-content: space-between; + margin-top: 0.5rem; + margin-left: 0.5rem; } .footer-company-details-address-wrapper { display: flex; - gap: 5px; + gap: 0px; margin-right: 60px; } @@ -173,8 +175,8 @@ #company-details { display: flex; flex-direction: column; - margin-top: 2rem; - margin-bottom: 2rem; + margin-top: 0.5rem; + margin-bottom: 0rem; } #company-address > *, @@ -226,8 +228,8 @@ } [data-ref="total_table-footer"] { - margin-top: 2rem; - margin-bottom: 2rem; + margin-top: 1rem; + margin-bottom: 1rem; } table { @@ -348,7 +350,7 @@ $entity_images