Merge pull request #6723 from turbo124/v5-stable

v5.3.17
This commit is contained in:
David Bomba 2021-09-26 13:34:37 +10:00 committed by GitHub
commit 5f7de014b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 384441 additions and 381771 deletions

View File

@ -54,4 +54,4 @@ PHANTOMJS_SECRET=secret
UPDATE_SECRET=secret
COMPOSER_AUTH='{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}'
SENTRY_LARAVEL_DSN=https://cc7e8e2c678041689e53e409b7dba236@sentry.invoicing.co/5
SENTRY_LARAVEL_DSN=https://32f01ea994744fa08a0f688769cef78a@sentry.invoicing.co/9

View File

@ -1 +1 @@
5.3.16
5.3.17

View File

@ -29,6 +29,7 @@ use App\Models\Expense;
use App\Models\Product;
use App\Models\Project;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Models\User;
use App\Models\Vendor;
@ -532,7 +533,9 @@ class CreateTestData extends Command
$invoice->save();
$invoice->service()->createInvitations()->markSent();
if (rand(0, 1)) {
$this->invoice_repo->markSent($invoice);
}
if (rand(0, 1)) {
$invoice = $invoice->service()->markPaid()->save();
@ -545,6 +548,9 @@ class CreateTestData extends Command
'documentable_id' => $invoice->id
]);
RecurringInvoice::factory()->create(['user_id' => $invoice->user->id, 'company_id' => $invoice->company->id, 'client_id' => $invoice->client_id]);
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}

View File

@ -693,13 +693,13 @@ class CompanySettings extends BaseSettings
'$invoice.date',
'$due_date',
'$total',
'$outstanding',
'$balance',
],
'statement_payment_columns' => [
'$invoice.number',
'$payment.date',
'$method',
'$outstanding',
'$statement_amount',
],
];

View File

@ -123,7 +123,7 @@ class EmailTemplateDefaults
public static function emailInvoiceTemplate()
{
$invoice_message = '<p>'.self::transformText('invoice_message').'</p><div class="center">$view_link</div>';
$invoice_message = '<p>'.self::transformText('invoice_message').'</p><div class="center">$view_button</div>';
return $invoice_message;
}
@ -135,7 +135,7 @@ class EmailTemplateDefaults
public static function emailQuoteTemplate()
{
$quote_message = '<p>'.self::transformText('quote_message').'</p><div class="center">$view_link</div>';
$quote_message = '<p>'.self::transformText('quote_message').'</p><div class="center">$view_button</div>';
return $quote_message;
}
@ -147,21 +147,21 @@ class EmailTemplateDefaults
public static function emailPaymentTemplate()
{
$payment_message = '<p>'.self::transformText('payment_message').'</p><div class="center">$view_link</div>';
$payment_message = '<p>'.self::transformText('payment_message').'</p><div class="center">$view_button</div>';
return $payment_message;
}
public static function emailCreditTemplate()
{
$credit_message = '<p>'.self::transformText('credit_message').'</p><div class="center">$view_link</div>';
$credit_message = '<p>'.self::transformText('credit_message').'</p><div class="center">$view_button</div>';
return $credit_message;
}
public static function emailPaymentPartialTemplate()
{
$payment_message = '<p>'.self::transformText('payment_message').'</p><div class="center">$view_link</div>';
$payment_message = '<p>'.self::transformText('payment_message').'</p><div class="center">$view_button</div>';
return $payment_message;
}

View File

@ -29,7 +29,7 @@ class CloneQuoteToInvoiceFactory
unset($quote_array['id']);
unset($quote_array['invitations']);
unset($quote_array['terms']);
unset($quote_array['public_notes']);
// unset($quote_array['public_notes']);
unset($quote_array['footer']);
unset($quote_array['design_id']);

View File

@ -14,6 +14,7 @@ namespace App\Factory;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Utils\Helpers;
class RecurringInvoiceToInvoiceFactory
{
@ -24,14 +25,15 @@ class RecurringInvoiceToInvoiceFactory
$invoice->discount = $recurring_invoice->discount;
$invoice->is_amount_discount = $recurring_invoice->is_amount_discount;
$invoice->po_number = $recurring_invoice->po_number;
$invoice->footer = $recurring_invoice->footer;
$invoice->terms = $recurring_invoice->terms;
$invoice->public_notes = $recurring_invoice->public_notes;
$invoice->footer = self::tranformObject($recurring_invoice->footer, $client);
$invoice->terms = self::tranformObject($recurring_invoice->terms, $client);
$invoice->public_notes = self::tranformObject($recurring_invoice->public_notes, $client);
$invoice->private_notes = $recurring_invoice->private_notes;
//$invoice->date = now()->format($client->date_format());
//$invoice->due_date = $recurring_invoice->calculateDueDate(now());
$invoice->is_deleted = $recurring_invoice->is_deleted;
$invoice->line_items = $recurring_invoice->line_items;
// $invoice->line_items = $recurring_invoice->line_items;
$invoice->line_items = self::transformItems($recurring_invoice, $client);
$invoice->tax_name1 = $recurring_invoice->tax_name1;
$invoice->tax_rate1 = $recurring_invoice->tax_rate1;
$invoice->tax_name2 = $recurring_invoice->tax_name2;
@ -45,6 +47,7 @@ class RecurringInvoiceToInvoiceFactory
$invoice->custom_value3 = $recurring_invoice->custom_value3;
$invoice->custom_value4 = $recurring_invoice->custom_value4;
$invoice->amount = $recurring_invoice->amount;
$invoice->uses_inclusive_taxes = $recurring_invoice->uses_inclusive_taxes;
// $invoice->balance = $recurring_invoice->balance;
$invoice->user_id = $recurring_invoice->user_id;
$invoice->assigned_user_id = $recurring_invoice->assigned_user_id;
@ -57,4 +60,25 @@ class RecurringInvoiceToInvoiceFactory
return $invoice;
}
private static function transformItems($recurring_invoice, $client)
{
$line_items = $recurring_invoice->line_items;
foreach($line_items as $key => $item){
if(property_exists($line_items[$key], 'notes'))
$line_items[$key]->notes = Helpers::processReservedKeywords($item->notes, $client);
}
return $line_items;
}
private static function tranformObject($object, $client)
{
return Helpers::processReservedKeywords($object, $client);
}
}

View File

@ -223,7 +223,7 @@ class PaymentController extends Controller
$invoice_totals = $payable_invoices->sum('amount');
$first_invoice = $invoices->first();
$credit_totals = $first_invoice->client->getSetting('use_credits_payment') == 'always' ? $first_invoice->client->service()->getCreditBalance() : 0;
$starting_invoice_amount = $first_invoice->amount;
$starting_invoice_amount = $first_invoice->balance;
if ($gateway) {
$first_invoice->service()->addGatewayFee($gateway, $payment_method_id, $invoice_totals)->save();
@ -234,7 +234,7 @@ class PaymentController extends Controller
* by adding it as a line item, and then subtract
* the starting and finishing amounts of the invoice.
*/
$fee_totals = $first_invoice->amount - $starting_invoice_amount;
$fee_totals = $first_invoice->balance - $starting_invoice_amount;
if ($gateway) {
$tokens = auth()->user()->client->gateway_tokens()

View File

@ -0,0 +1,59 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Statements\ShowStatementRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
class StatementController extends Controller
{
/**
* Show the statement in the client portal.
*
* @return View
*/
public function index(): View
{
return render('statement.index');
}
/**
* Show the raw stream of the PDF.
*
* @param ShowStatementRequest $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|JsonResponse|\Illuminate\Http\Response|StreamedResponse
*/
public function raw(ShowStatementRequest $request)
{
$pdf = $request->client()->service()->statement(
$request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table'])
);
if ($pdf && $request->query('download')) {
return response()->streamDownload(function () use ($pdf) {
echo $pdf;
}, 'statement.pdf', ['Content-Type' => 'application/pdf']);
}
if ($pdf) {
return response($pdf, 200)->withHeaders([
'Content-Type' => 'application/pdf',
]);
}
return response()->json(['message' => 'Something went wrong. Please check logs.']);
}
}

View File

@ -110,7 +110,9 @@ class ClientStatementController extends BaseController
public function statement(CreateStatementRequest $request)
{
$pdf = $this->createStatement($request);
$pdf = $request->client()->service()->statement(
$request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table'])
);
if ($pdf) {
return response()->streamDownload(function () use ($pdf) {
@ -120,88 +122,4 @@ class ClientStatementController extends BaseController
return response()->json(['message' => 'Something went wrong. Please check logs.']);
}
protected function createStatement(CreateStatementRequest $request): ?string
{
$invitation = false;
if ($request->getInvoices()->count() >= 1) {
$this->entity = $request->getInvoices()->first();
$invitation = $this->entity->invitations->first();
}
else if ($request->getPayments()->count() >= 1) {
$this->entity = $request->getPayments()->first()->invoices->first()->invitations->first();
$invitation = $this->entity->invitations->first();
}
$entity_design_id = 1;
$entity_design_id = $this->entity->design_id
? $this->entity->design_id
: $this->decodePrimaryKey($this->entity->client->getSetting('invoice_design_id'));
$design = Design::find($entity_design_id);
if (!$design) {
$design = Design::find($entity_design_id);
}
$html = new HtmlEngine($invitation);
$options = [
'start_date' => $request->start_date,
'end_date' => $request->end_date,
'show_payments_table' => $request->show_payments_table,
'show_aging_table' => $request->show_aging_table,
];
if ($design->is_custom) {
$options['custom_partials'] = \json_decode(\json_encode($design->design), true);
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
} else {
$template = new PdfMakerDesign(strtolower($design->name), $options);
}
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => $this->entity->client,
'entity' => $this->entity,
'pdf_variables' => (array)$this->entity->company->settings->pdf_variables,
'$product' => $design->design->product,
'variables' => $variables,
'invoices' => $request->getInvoices(),
'payments' => $request->getPayments(),
'aging' => $request->getAging(),
], \App\Services\PdfMaker\Design::STATEMENT),
'variables' => $variables,
'options' => [],
'process_markdown' => $this->entity->client->company->markdown_enabled,
];
$maker = new PdfMakerService($state);
$maker
->design($template)
->build();
$pdf = null;
try {
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
else if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
} else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}
return $pdf;
}
}

View File

@ -589,14 +589,20 @@ class CreditController extends BaseController
public function downloadPdf($invitation_key)
{
$invitation = $this->credit_repository->getInvitationByKey($invitation_key);
// $contact = $invitation->contact;
$credit = $invitation->credit;
$file = $credit->service()->getCreditPdf($invitation);
$headers = ['Content-Type' => 'application/pdf'];
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
}, basename($file), $headers);
}

View File

@ -115,8 +115,8 @@ class EmailController extends BaseController
{
$entity = $request->input('entity');
$entity_obj = $entity::find($request->input('entity_id'));
$subject = $request->input('subject');
$body = $request->input('body');
$subject = $request->has('subject') ? $request->input('subject') : '';
$body = $request->has('body') ? $request->input('body') : '';
$entity_string = strtolower(class_basename($entity_obj));
$template = str_replace("email_template_", "", $request->input('template'));

View File

@ -804,9 +804,15 @@ class InvoiceController extends BaseController
$file = $invoice->service()->getInvoicePdf($contact);
$headers = ['Content-Type' => 'application/pdf'];
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
}, basename($file), $headers);
}
/**

View File

@ -212,7 +212,7 @@ class PreviewController extends BaseController
}
$entity_obj = $repo->save($request->all(), $entity_obj);
$entity_obj->service()->fillDefaults()->save();
$entity_obj->load('client');
App::forgetInstance('translator');

View File

@ -740,11 +740,16 @@ class QuoteController extends BaseController
$file = $quote->service()->getQuotePdf($contact);
$headers = ['Content-Type' => 'application/pdf'];
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
}, basename($file), $headers);
// return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -29,6 +29,7 @@ class SubdomainController extends BaseController
'preview',
'invoiceninja',
'cname',
'sandbox',
];
public function __construct()

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests\ClientPortal\Statements;
use App\Models\Client;
use Illuminate\Foundation\Http\FormRequest;
class ShowStatementRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'show_payments_table' => $this->has('show_payments_table') ? \boolval($this->show_payments_table) : false,
'show_aging_table' => $this->has('show_aging_table') ? \boolval($this->show_aging_table) : false,
]);
}
public function client(): Client
{
return auth('contact')->user()->client;
}
}

View File

@ -22,7 +22,7 @@ class DefaultCompanyRequest extends Request
*/
public function authorize() : bool
{
return auth()->user()->isAdmin()
return auth()->user()->isAdmin();
}
public function rules()

View File

@ -39,8 +39,8 @@ class SendEmailRequest extends Request
'template' => 'required',
'entity' => 'required',
'entity_id' => 'required',
'subject' => 'required',
'body' => 'required',
// 'subject' => 'required',
// 'body' => 'required',
];
}

View File

@ -4,11 +4,7 @@ namespace App\Http\Requests\Statements;
use App\Http\Requests\Request;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Payment;
use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
class CreateStatementRequest extends Request
{
@ -38,6 +34,7 @@ class CreateStatementRequest extends Request
'show_aging_table' => 'boolean',
];
}
protected function prepareForValidation()
{
$input = $this->all();
@ -45,125 +42,15 @@ class CreateStatementRequest extends Request
$input = $this->decodePrimaryKeys($input);
$this->replace($input);
$this->merge([
'show_payments_table' => $this->has('show_payments_table') ? \boolval($this->show_payments_table) : false,
'show_aging_table' => $this->has('show_aging_table') ? \boolval($this->show_aging_table) : false,
]);
}
/**
* The collection of invoices for the statement.
*
* @return Invoice[]|\Illuminate\Database\Eloquent\Collection
*/
public function getInvoices()
public function client(): ?Client
{
$input = $this->all();
// $input['start_date & $input['end_date are available.
$client = Client::where('id', $input['client_id'])->first();
$from = Carbon::parse($input['start_date']);
$to = Carbon::parse($input['end_date']);
return Invoice::where('company_id', auth()->user()->company()->id)
->where('client_id', $client->id)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID])
->whereBetween('date',[$from, $to])
->get();
}
/**
* The collection of payments for the statement.
*
* @return Payment[]|\Illuminate\Database\Eloquent\Collection
*/
public function getPayments()
{
// $input['start_date & $input['end_date are available.
$input = $this->all();
$client = Client::where('id', $input['client_id'])->first();
$from = Carbon::parse($input['start_date']);
$to = Carbon::parse($input['end_date']);
return Payment::where('company_id', auth()->user()->company()->id)
->where('client_id', $client->id)
->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->whereBetween('date',[$from, $to])
->get();
}
/**
* The array of aging data.
*/
public function getAging(): array
{
return [
'0-30' => $this->getAgingAmount('30'),
'30-60' => $this->getAgingAmount('60'),
'60-90' => $this->getAgingAmount('90'),
'90-120' => $this->getAgingAmount('120'),
'120+' => $this->getAgingAmount('120+'),
];
}
private function getAgingAmount($range)
{
$input = $this->all();
$ranges = $this->calculateDateRanges($range);
$from = $ranges[0];
$to = $ranges[1];
$client = Client::where('id', $input['client_id'])->first();
$amount = Invoice::where('company_id', auth()->user()->company()->id)
->where('client_id', $client->id)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereBetween('date',[$from, $to])
->sum('balance');
return Number::formatMoney($amount, $client);
}
private function calculateDateRanges($range)
{
$ranges = [];
switch ($range) {
case '30':
$ranges[0] = now();
$ranges[1] = now()->subDays(30);
return $ranges;
break;
case '60':
$ranges[0] = now()->subDays(30);
$ranges[1] = now()->subDays(60);
return $ranges;
break;
case '90':
$ranges[0] = now()->subDays(60);
$ranges[1] = now()->subDays(90);
return $ranges;
break;
case '120':
$ranges[0] = now()->subDays(90);
$ranges[1] = now()->subDays(120);
return $ranges;
break;
case '120+':
$ranges[0] = now()->subDays(120);
$ranges[1] = now()->subYears(40);
return $ranges;
break;
default:
$ranges[0] = now()->subDays(0);
$ranges[1] = now()->subDays(30);
return $ranges;
break;
}
return Client::where('id', $this->client_id)->first();
}
}

View File

@ -119,6 +119,8 @@ class PortalComposer
$data[] = ['title' => ctrans('texts.tasks'), 'url' => 'client.tasks.index', 'icon' => 'clock'];
}
$data[] = ['title' => ctrans('texts.statement'), 'url' => 'client.statement', 'icon' => 'activity'];
return $data;
}
}

View File

@ -514,7 +514,7 @@ class CompanyExport implements ShouldQueue
$nmo->company = $company_reference;
$nmo->settings = $this->company->settings;
NinjaMailerJob::dispatch($nmo);
NinjaMailerJob::dispatch($nmo, true);
if(Ninja::isHosted()){
sleep(3);

View File

@ -13,6 +13,7 @@ namespace App\Jobs\Company;
use App\Exceptions\ImportCompanyFailed;
use App\Exceptions\NonExistingMigrationFile;
use App\Factory\ClientContactFactory;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\UnlinkFile;
@ -65,14 +66,15 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use JsonMachine\JsonDecoder\ExtJsonDecoder;
use JsonMachine\JsonMachine;
use ZipArchive;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use JsonMachine\JsonMachine;
use JsonMachine\JsonDecoder\ExtJsonDecoder;
class CompanyImport implements ShouldQueue
{
@ -208,7 +210,8 @@ class CompanyImport implements ShouldQueue
$this->preFlightChecks()
->purgeCompanyData()
->importData();
->importData()
->postImportCleanup();
$data = [
'errors' => []
@ -234,12 +237,32 @@ class CompanyImport implements ShouldQueue
}
//
private function postImportCleanup()
{
//ensure all clients have a contact
$this->company
->clients()
->whereDoesntHave('contacts')
->cursor()
->each(function ($client){
$new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
$new_contact->client_id = $client->id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->confirmed = true;
$new_contact->email = ' ';
$new_contact->save();
});
}
private function unzipFile()
{
// if(mime_content_type(Storage::path($this->file_location)) == 'text/plain')
// return Storage::path($this->file_location);
$path = TempFile::filePath(Storage::disk(config('filesystems.default'))->get($this->file_location), basename($this->file_location));
$zip = new ZipArchive();
@ -1391,6 +1414,9 @@ class CompanyImport implements ShouldQueue
private function sendImportMail($message){
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new CompanyImportFailure($this->company, $message);

View File

@ -38,6 +38,7 @@ use App\Repositories\BaseRepository;
use App\Repositories\ClientRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\PaymentRepository;
use App\Utils\Ninja;
use App\Utils\Traits\CleanLineItems;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -52,6 +53,7 @@ use League\Csv\Reader;
use League\Csv\Statement;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Illuminate\Support\Facades\App;
class CSVImport implements ShouldQueue {
@ -132,6 +134,10 @@ class CSVImport implements ShouldQueue {
'company' => $this->company,
];
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new ImportCompleted($this->company, $data);
$nmo->company = $this->company;

View File

@ -19,13 +19,14 @@ use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesInvoiceValues;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Turbo124\Beacon\Facades\LightLogs;
use Carbon\Carbon;
class SendRecurring implements ShouldQueue
{

View File

@ -120,7 +120,7 @@ class WebhookHandler implements ShouldQueue
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_SUCCESS,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->company->clients->first(),
$this->resolveClient(),
$this->company
);
@ -137,7 +137,7 @@ class WebhookHandler implements ShouldQueue
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_RESPONSE,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->company->clients->first(),
$this->resolveClient(),
$this->company,
);
@ -145,6 +145,15 @@ class WebhookHandler implements ShouldQueue
}
private function resolveClient()
{
if($this->entity->client()->exists()){
return $this->entity->client;
}
return $this->company->clients->first();
}
public function failed($exception)
{
nlog(print_r($exception->getMessage(), 1));

View File

@ -135,6 +135,7 @@ class PaymentEmailEngine extends BaseEmailEngine
{
$data = [];
$data['$from'] = ['value' => '', 'label' => ctrans('texts.from')];
$data['$to'] = ['value' => '', 'label' => ctrans('texts.to')];
$data['$number'] = ['value' => $this->payment->number ?: '&nbsp;', 'label' => ctrans('texts.payment_number')];
@ -227,6 +228,7 @@ class PaymentEmailEngine extends BaseEmailEngine
$data['$company4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company4', $this->settings->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company4')];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->payment->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')];
$data['$view_button'] = &$data['$view_link'];
$data['$paymentLink'] = &$data['$view_link'];
$data['$portalButton'] = ['value' => "<a href='{$this->payment->getPortalLink()}'>".ctrans('texts.login')."</a>", 'label' =>''];
$data['$portal_url'] = &$data['$portalButton'];

View File

@ -90,11 +90,17 @@ class Gateway extends StaticModel
case 7:
return [
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], // Mollie
GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true],
];
case 15:
return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal
break;
case 20:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded']],
GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false],
GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false],
GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']]]; //Stripe
case 39:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Checkout
break;

View File

@ -42,6 +42,7 @@ class PaymentType extends StaticModel
const SEPA = 29;
const GOCARDLESS = 30;
const CRYPTO = 31;
const MOLLIE_BANK_TRANSFER = 34;
public static function parseCardType($cardName)
{

View File

@ -55,7 +55,10 @@ class CompanyGatewayObserver
public function restored(CompanyGateway $company_gateway)
{
//When we restore the gateway, bring back the tokens!
ClientGatewayToken::where('company_gateway_id', $company_gateway->id)->withTrashed()->get()->restore();
ClientGatewayToken::where('company_gateway_id', $company_gateway->id)
->withTrashed()->cursor()->each(function ($cgt){
$cgt->restore();
});
}
/**

View File

@ -30,7 +30,7 @@ class InvoiceObserver
public function created(Invoice $invoice)
{
$subscriptions = Webhook::where('company_id', $invoice->company->id)
$subscriptions = Webhook::where('company_id', $invoice->company_id)
->where('event_id', Webhook::EVENT_CREATE_INVOICE)
->exists();
@ -47,7 +47,7 @@ class InvoiceObserver
*/
public function updated(Invoice $invoice)
{
$subscriptions = Webhook::where('company_id', $invoice->company->id)
$subscriptions = Webhook::where('company_id', $invoice->company_id)
->where('event_id', Webhook::EVENT_UPDATE_INVOICE)
->exists();
@ -65,7 +65,7 @@ class InvoiceObserver
*/
public function deleted(Invoice $invoice)
{
$subscriptions = Webhook::where('company_id', $invoice->company->id)
$subscriptions = Webhook::where('company_id', $invoice->company_id)
->where('event_id', Webhook::EVENT_DELETE_INVOICE)
->exists();

View File

@ -29,6 +29,9 @@ class PaymentObserver
->where('event_id', Webhook::EVENT_CREATE_PAYMENT)
->exists();
if($payment->invoices()->exists())
$payment->load('invoices');
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_CREATE_PAYMENT, $payment, $payment->company);
}

View File

@ -151,6 +151,11 @@ class CreditCard
],
];
if ($this->braintree->company_gateway->getConfigField('merchantAccountId')) {
/** https://developer.paypal.com/braintree/docs/reference/request/transaction/sale/php#full-example */
$data['options']['verificationMerchantAccountId'] = $this->braintree->company_gateway->getConfigField('merchantAccountId');
}
$response = $this->braintree->gateway->paymentMethod()->create($data);
if ($response->success) {

View File

@ -95,7 +95,7 @@ class Token
$response_status = ErrorCode::getStatus($response->ResponseMessage);
$error = $response_status['message']
$error = $response_status['message'];
$error_code = $response->ResponseMessage;
$data = [

View File

@ -0,0 +1,212 @@
<?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\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\MolliePaymentDriver;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\View\View;
use Mollie\Api\Resources\Payment as ResourcesPayment;
class BankTransfer implements MethodInterface
{
protected MolliePaymentDriver $mollie;
public function __construct(MolliePaymentDriver $mollie)
{
$this->mollie = $mollie;
$this->mollie->init();
}
/**
* Show the authorization page for bank transfer.
*
* @param array $data
* @return View
*/
public function authorizeView(array $data): View
{
return render('gateways.mollie.bank_transfer.authorize', $data);
}
/**
* Handle the authorization for bank transfer.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
/**
* Show the payment page for bank transfer.
*
* @param array $data
* @return Redirector|RedirectResponse
*/
public function paymentView(array $data)
{
$this->mollie->payment_hash
->withData('gateway_type_id', GatewayType::BANK_TRANSFER)
->withData('client_id', $this->mollie->client->id);
try {
$payment = $this->mollie->gateway->payments->create([
'method' => 'banktransfer',
'amount' => [
'currency' => $this->mollie->client->currency()->code,
'value' => $this->mollie->convertToMollieAmount((float) $this->mollie->payment_hash->data->amount_with_fee),
],
'description' => \sprintf('Invoices: %s', collect($data['invoices'])->pluck('invoice_number')),
'redirectUrl' => route('client.payments.response', [
'company_gateway_id' => $this->mollie->company_gateway->id,
'payment_hash' => $this->mollie->payment_hash->hash,
'payment_method_id' => GatewayType::BANK_TRANSFER,
]),
'webhookUrl' => $this->mollie->company_gateway->webhookUrl(),
'metadata' => [
'client_id' => $this->mollie->client->hashed_id,
],
]);
$this->mollie->payment_hash->withData('payment_id', $payment->id);
return redirect(
$payment->getCheckoutUrl()
);
} catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) {
return $this->processUnsuccessfulPayment($exception);
}
}
/**
* Handle unsuccessful payment.
*
* @param Exception $e
* @throws PaymentFailed
* @return void
*/
public function processUnsuccessfulPayment(\Exception $e): void
{
PaymentFailureMailer::dispatch(
$this->mollie->client,
$e->getMessage(),
$this->mollie->client->company,
$this->mollie->payment_hash->data->amount_with_fee
);
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_MOLLIE,
$this->mollie->client,
$this->mollie->client->company,
);
throw new PaymentFailed($e->getMessage(), $e->getCode());
}
/**
* Handle the payments for the bank transfer.
*
* @param PaymentResponseRequest $request
* @return mixed
*/
public function paymentResponse(PaymentResponseRequest $request)
{
if (! \property_exists($this->mollie->payment_hash->data, 'payment_id')) {
return $this->processUnsuccessfulPayment(
new PaymentFailed('Whoops, something went wrong. Missing required [payment_id] parameter. Please contact administrator. Reference hash: ' . $this->mollie->payment_hash->hash)
);
}
try {
$payment = $this->mollie->gateway->payments->get(
$this->mollie->payment_hash->data->payment_id
);
if ($payment->status === 'paid') {
return $this->processSuccessfulPayment($payment);
}
if ($payment->status === 'open') {
return $this->processOpenPayment($payment);
}
return $this->processUnsuccessfulPayment(
new PaymentFailed(ctrans('texts.status_voided'))
);
} catch (\Mollie\Api\Exceptions\ApiException | \Exception $exception) {
return $this->processUnsuccessfulPayment($exception);
}
}
/**
* Handle the successful payment for bank transfer.
*
* @param ResourcesPayment $payment
* @param string $status
* @return RedirectResponse
*/
public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment, $status = 'paid'): RedirectResponse
{
$data = [
'gateway_type_id' => GatewayType::BANK_TRANSFER,
'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total,
'payment_type' => PaymentType::MOLLIE_BANK_TRANSFER,
'transaction_reference' => $payment->id,
];
$payment_record = $this->mollie->createPayment(
$data,
$status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING
);
SystemLogger::dispatch(
['response' => $payment, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_MOLLIE,
$this->mollie->client,
$this->mollie->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]);
}
/**
* Handle 'open' payment status for bank transfer.
*
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processOpenPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse
{
return $this->processSuccessfulPayment($payment, 'open');
}
}

View File

@ -24,6 +24,7 @@ use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Mollie\BankTransfer;
use App\PaymentDrivers\Mollie\CreditCard;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Validator;
@ -64,6 +65,7 @@ class MolliePaymentDriver extends BaseDriver
*/
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
GatewayType::BANK_TRANSFER => BankTransfer::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE;
@ -84,6 +86,7 @@ class MolliePaymentDriver extends BaseDriver
$types = [];
$types[] = GatewayType::CREDIT_CARD;
$types[] = GatewayType::BANK_TRANSFER;
return $types;
}

View File

@ -77,31 +77,36 @@ class Token
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$amount = round(($amount * pow(10, $this->payfast->client->currency()->precision)),0);
$header =[
'merchant-id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'timestamp' => now()->format('c'),
'version' => 'v1',
];
// $header =[
// 'merchant-id' => $this->payfast->company_gateway->getConfigField('merchantId'),
// 'timestamp' => now()->format('c'),
// 'version' => 'v1',
// ];
nlog($header);
// $body = [
// 'amount' => $amount,
// 'item_name' => 'purchase',
// 'item_description' => ctrans('texts.invoices') . ': ' . collect($payment_hash->invoices())->pluck('invoice_number'),
// // 'm_payment_id' => $payment_hash->hash,
// ];
$body = [
'amount' => $amount,
'item_name' => 'purchase',
'item_description' => ctrans('texts.invoices') . ': ' . collect($payment_hash->invoices())->pluck('invoice_number'),
'm_payment_id' => $payment_hash->hash,
'passphrase' => $this->payfast->company_gateway->getConfigField('passphrase'),
];
// $header['signature'] = md5( $this->generate_parameter_string(array_merge($header, $body), false) );
$header['signature'] = $this->genSig(array_merge($header, $body));
// $result = $this->send($header, $body, $cgt->token);
$api = new \PayFast\PayFastApi(
[
'merchantId' => $this->payfast->company_gateway->getConfigField('merchantId'),
'passPhrase' => $this->payfast->company_gateway->getConfigField('passPhrase'),
'testMode' => $this->payfast->company_gateway->getConfigField('testMode')
]
);
nlog($header['signature']);
nlog($header['timestamp']);
nlog($this->payfast->company_gateway->getConfigField('merchantId'));
$adhocArray = $api
->subscriptions
->adhoc($cgt->token, ['amount' => $amount, 'item_name' => 'purchase']);
$result = $this->send($header, $body, $cgt->token);
nlog($result);
nlog($adhocArray);
// /*Refactor and push to BaseDriver*/
// if ($data['response'] != null && $data['response']->getMessages()->getResultCode() == 'Ok') {
@ -143,6 +148,44 @@ class Token
// }
}
protected function generate_parameter_string( $api_data, $sort_data_before_merge = true, $skip_empty_values = true ) {
// if sorting is required the passphrase should be added in before sort.
if ( ! empty( $this->payfast->company_gateway->getConfigField('passPhrase') ) && $sort_data_before_merge )
$api_data['passphrase'] = $this->payfast->company_gateway->getConfigField('passPhrase');
if ( $sort_data_before_merge ) {
ksort( $api_data );
}
// concatenate the array key value pairs.
$parameter_string = '';
foreach ( $api_data as $key => $val ) {
if ( $skip_empty_values && empty( $val ) ) {
continue;
}
if ( 'signature' !== $key ) {
$val = urlencode( $val );
$parameter_string .= "$key=$val&";
}
}
// when not sorting passphrase should be added to the end before md5
if ( $sort_data_before_merge ) {
$parameter_string = rtrim( $parameter_string, '&' );
} elseif ( ! empty( $this->pass_phrase ) ) {
$parameter_string .= 'passphrase=' . urlencode( $this->payfast->company_gateway->getConfigField('passPhrase') );
} else {
$parameter_string = rtrim( $parameter_string, '&' );
}
nlog($parameter_string);
return $parameter_string;
}
private function genSig($data)
{
$fields = [];

View File

@ -77,7 +77,7 @@ class PayFastPaymentDriver extends BaseDriver
]
);
} catch(Exception $e) {
} catch(\Exception $e) {
echo '##PAYFAST## There was an exception: '.$e->getMessage();
@ -161,6 +161,8 @@ class PayFastPaymentDriver extends BaseDriver
}
}
nlog(http_build_query($fields));
return md5(http_build_query($fields));
}

View File

@ -63,7 +63,7 @@ class CreditCard
'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'currency' => $this->stripe->client->getCurrencyCode(),
'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $this->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')),
'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')),
];
$payment_intent_data['setup_future_usage'] = 'off_session';

View File

@ -43,7 +43,7 @@ class SEPA
'customer' => $customer->id,
], $this->stripe_driver->stripe_connect_auth);
$client_secret = $setup_intent->client_secret
$client_secret = $setup_intent->client_secret;
// Pass the client secret to the client

View File

@ -24,7 +24,7 @@ use App\PaymentDrivers\StripePaymentDriver;
class SOFORT
{
/** @var StripePaymentDriver */
public $stripe;
public StripePaymentDriver $stripe;
public function __construct(StripePaymentDriver $stripe)
{
@ -45,6 +45,17 @@ class SOFORT
$data['customer'] = $this->stripe->findOrCreateCustomer()->id;
$data['country'] = $this->stripe->client->country->iso_3166_2;
$intent = \Stripe\PaymentIntent::create([
'amount' => $data['stripe_amount'],
'currency' => 'eur',
'payment_method_types' => ['sofort'],
'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')),
]);
$data['pi_client_secret'] = $intent->client_secret;
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]);
$this->stripe->payment_hash->save();
@ -66,23 +77,23 @@ class SOFORT
$this->stripe->payment_hash->save();
if ($request->redirect_status == 'succeeded') {
return $this->processSuccessfulPayment($request->source);
return $this->processSuccessfulPayment($request->payment_intent);
}
return $this->processUnsuccessfulPayment();
}
public function processSuccessfulPayment(string $source)
public function processSuccessfulPayment(string $payment_intent)
{
/* @todo: https://github.com/invoiceninja/invoiceninja/pull/3789/files#r436175798 */
$this->stripe->init();
$data = [
'payment_method' => $this->stripe->payment_hash->data->source,
'payment_method' => $payment_intent,
'payment_type' => PaymentType::SOFORT,
'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' => $payment_intent,
'gateway_type_id' => GatewayType::SOFORT,
];

View File

@ -423,7 +423,7 @@ class StripePaymentDriver extends BaseDriver
// Allow app to catch up with webhook request.
sleep(2);
if ($request->type === 'charge.succeeded') {
if ($request->type === 'charge.succeeded' || $request->type === 'payment_intent.succeeded') {
foreach ($request->data as $transaction) {
$payment = Payment::query()
@ -634,4 +634,14 @@ class StripePaymentDriver extends BaseDriver
return response()->json(['message' => 'success'], 200);
}
public function decodeUnicodeString($string)
{
return html_entity_decode($string, ENT_QUOTES, 'UTF-8');
// return iconv("UTF-8", "ISO-8859-1//TRANSLIT", $this->decode_encoded_utf8($string));
}
public function decode_encoded_utf8($string){
return preg_replace_callback('#\\\\u([0-9a-f]{4})#ism', function($matches) { return mb_convert_encoding(pack("H*", $matches[1]), "UTF-8", "UCS-2BE"); }, $string);
}
}

View File

@ -85,6 +85,16 @@ class ClientService
return $this;
}
/**
* Generate the client statement.
*
* @param array $options
*/
public function statement(array $options = [])
{
return (new Statement($this->client, $options))->run();
}
public function save() :Client
{
$this->client->save();

View File

@ -0,0 +1,353 @@
<?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\Services\Client;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceInvitationFactory;
use App\Factory\InvoiceItemFactory;
use App\Models\Client;
use App\Models\Design;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Product;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Number;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
class Statement
{
use PdfMakerTrait;
protected Client $client;
/**
* @var Invoice|Payment|null
*/
protected $entity;
protected array $options;
protected bool $rollback = false;
public function __construct(Client $client, array $options)
{
$this->client = $client;
$this->options = $options;
}
public function run(): ?string
{
$this
->setupOptions()
->setupEntity();
$html = new HtmlEngine($this->getInvitation());
if ($this->getDesign()->is_custom) {
$this->options['custom_partials'] = \json_decode(\json_encode($this->getDesign()->design), true);
$template = new PdfMakerDesign(\App\Services\PdfMaker\Design::CUSTOM, $this->options);
} else {
$template = new PdfMakerDesign(strtolower($this->getDesign()->name), $this->options);
}
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => $this->entity->client,
'entity' => $this->entity,
'pdf_variables' => (array)$this->entity->company->settings->pdf_variables,
'$product' => $this->getDesign()->design->product,
'variables' => $variables,
'invoices' => $this->getInvoices(),
'payments' => $this->getPayments(),
'aging' => $this->getAging(),
], \App\Services\PdfMaker\Design::STATEMENT),
'variables' => $variables,
'options' => [],
'process_markdown' => $this->entity->client->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($template)
->build();
$pdf = null;
try {
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
} elseif (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
} else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}
if ($this->rollback) {
DB::rollBack();
}
return $pdf;
}
/**
* Setup correct entity instance.
*
* @return Statement
*/
protected function setupEntity(): self
{
if (count($this->getInvoices()) >= 1) {
$this->entity = $this->getInvoices()->first();
}
if (\is_null($this->entity)) {
DB::beginTransaction();
$this->rollback = true;
$invoice = InvoiceFactory::create($this->client->company->id, $this->client->user->id);
$invoice->client_id = $this->client->id;
$invoice->line_items = $this->buildLineItems();
$invoice->save();
$invitation = InvoiceInvitationFactory::create($invoice->company_id, $invoice->user_id);
$invitation->invoice_id = $invoice->id;
$invitation->client_contact_id = $this->client->contacts->first()->id;
$invitation->save();
$this->entity = $invoice;
}
return $this;
}
protected function buildLineItems($count = 1)
{
$line_items = [];
for ($x = 0; $x < $count; $x++) {
$item = InvoiceItemFactory::create();
$item->quantity = 1;
//$item->cost = 10;
if (rand(0, 1)) {
$item->tax_name1 = 'GST';
$item->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$item->tax_name1 = 'VAT';
$item->tax_rate1 = 17.50;
}
if (rand(0, 1)) {
$item->tax_name1 = 'Sales Tax';
$item->tax_rate1 = 5;
}
$product = Product::all()->random();
$item->cost = (float) $product->cost;
$item->product_key = $product->product_key;
$item->notes = $product->notes;
$item->custom_value1 = $product->custom_value1;
$item->custom_value2 = $product->custom_value2;
$item->custom_value3 = $product->custom_value3;
$item->custom_value4 = $product->custom_value4;
$line_items[] = $item;
}
return $line_items;
}
/**
* Setup & prepare options.
*
* @return Statement
*/
protected function setupOptions(): self
{
if (! \array_key_exists('start_date', $this->options)) {
$this->options['start_date'] = now()->startOfYear()->format('Y-m-d');
}
if (! \array_key_exists('end_date', $this->options)) {
$this->options['end_date'] = now()->format('Y-m-d');
}
if (! \array_key_exists('show_payments_table', $this->options)) {
$this->options['show_payments_table'] = false;
}
if (! \array_key_exists('show_aging_table', $this->options)) {
$this->options['show_aging_table'] = false;
}
return $this;
}
/**
* The collection of invoices for the statement.
*
* @return Invoice[]|\Illuminate\Database\Eloquent\Collection
*/
protected function getInvoices(): Collection
{
return Invoice::where('company_id', $this->client->company->id)
->where('client_id', $this->client->id)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID])
->whereBetween('date', [$this->options['start_date'], $this->options['end_date']])
->orderBy('number', 'ASC')
->get();
}
/**
* The collection of payments for the statement.
*
* @return Payment[]|\Illuminate\Database\Eloquent\Collection
*/
protected function getPayments(): Collection
{
return Payment::where('company_id', $this->client->company->id)
->where('client_id', $this->client->id)
->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->whereBetween('date', [$this->options['start_date'], $this->options['end_date']])
->orderBy('number', 'ASC')
->get();
}
/**
* Get correct invitation ID.
*
* @return int|bool
*/
protected function getInvitation()
{
if ($this->entity instanceof Invoice || $this->entity instanceof Payment) {
return $this->entity->invitations->first();
}
return false;
}
/**
* Get the array of aging data.
*
* @return array
*/
protected function getAging(): array
{
return [
'0-30' => $this->getAgingAmount('30'),
'30-60' => $this->getAgingAmount('60'),
'60-90' => $this->getAgingAmount('90'),
'90-120' => $this->getAgingAmount('120'),
'120+' => $this->getAgingAmount('120+'),
];
}
/**
* Generate aging amount.
*
* @param mixed $range
* @return string
*/
private function getAgingAmount($range)
{
$ranges = $this->calculateDateRanges($range);
$from = $ranges[0];
$to = $ranges[1];
$client = Client::where('id', $this->client->id)->first();
$amount = Invoice::where('company_id', $this->client->company->id)
->where('client_id', $client->id)
->where('company_id', $this->client->company_id)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->where('is_deleted', 0)
->whereBetween('date', [$to, $from])
->sum('balance');
return Number::formatMoney($amount, $client);
}
/**
* Calculate date ranges for aging.
*
* @param mixed $range
* @return array
*/
private function calculateDateRanges($range)
{
$ranges = [];
switch ($range) {
case '30':
$ranges[0] = now()->startOfDay();
$ranges[1] = now()->startOfDay()->subDays(30);
return $ranges;
case '60':
$ranges[0] = now()->startOfDay()->subDays(30);
$ranges[1] = now()->startOfDay()->subDays(60);
return $ranges;
case '90':
$ranges[0] = now()->startOfDay()->subDays(60);
$ranges[1] = now()->startOfDay()->subDays(90);
return $ranges;
case '120':
$ranges[0] = now()->startOfDay()->subDays(90);
$ranges[1] = now()->startOfDay()->subDays(120);
return $ranges;
case '120+':
$ranges[0] = now()->startOfDay()->subDays(120);
$ranges[1] = now()->startOfDay()->subYears(40);
return $ranges;
default:
$ranges[0] = now()->startOfDay()->subDays(0);
$ranges[1] = now()->subDays(30);
return $ranges;
}
}
/**
* Get correct design for statement.
*
* @return \App\Models\Design
*/
protected function getDesign(): Design
{
$id = 1;
if (!empty($this->client->getSetting('entity_design_id'))) {
$id = (int) $this->client->getSetting('entity_design_id');
}
return Design::find($id);
}
}

View File

@ -70,7 +70,7 @@ class AutoBillInvoice extends AbstractService
/* Determine $amount */
if ($this->invoice->partial > 0) {
$is_partial = true;
$invoice_total = $this->invoice->amount;
$invoice_total = $this->invoice->balance;
$amount = $this->invoice->partial;
} elseif ($this->invoice->balance > 0) {
$amount = $this->invoice->balance;
@ -94,14 +94,18 @@ class AutoBillInvoice extends AbstractService
/* $gateway fee */
$this->invoice = $this->invoice->service()->addGatewayFee($gateway_token->gateway, $gateway_token->gateway_type_id, $amount)->save();
//change from $this->invoice->amount to $this->invoice->balance
if($is_partial)
$fee = $this->invoice->amount - $invoice_total;
$fee = $this->invoice->balance - $invoice_total;
else
$fee = $this->invoice->amount - $amount;
$fee = $this->invoice->balance - $amount;
if($fee > $amount)
$fee = 0;
/* Build payment hash */
$payment_hash = PaymentHash::create([
'hash' => Str::random(128),
'hash' => Str::random(64),
'data' => ['invoices' => [['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount]]],
'fee_total' => $fee,
'fee_invoice_id' => $this->invoice->id,

View File

@ -28,6 +28,8 @@ class LedgerService
{
$balance = 0;
\DB::connection(config('database.default'))->beginTransaction();
$company_ledger = $this->ledger();
if ($company_ledger) {
@ -44,6 +46,8 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
\DB::connection(config('database.default'))->commit();
return $this;
}
@ -51,6 +55,8 @@ class LedgerService
{
$balance = 0;
\DB::connection(config('database.default'))->beginTransaction();
/* Get the last record for the client and set the current balance*/
$company_ledger = $this->ledger();
@ -68,6 +74,8 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
\DB::connection(config('database.default'))->commit();
return $this;
}
@ -75,6 +83,8 @@ class LedgerService
{
$balance = 0;
\DB::connection(config('database.default'))->beginTransaction();
$company_ledger = $this->ledger();
if ($company_ledger) {
@ -91,6 +101,8 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
\DB::connection(config('database.default'))->commit();
return $this;
}
@ -99,6 +111,7 @@ class LedgerService
return CompanyLedger::whereClientId($this->entity->client_id)
->whereCompanyId($this->entity->company_id)
->orderBy('id', 'DESC')
->lockForUpdate()
->first();
}
@ -109,3 +122,11 @@ class LedgerService
return $this->entity;
}
}
/*
DB::connection(config('database.default'))->beginTransaction();
\DB::connection(config('database.default'))->commit();
*/

View File

@ -232,7 +232,7 @@ class Design extends BaseDesign
]],
['element' => 'tr', 'properties' => [], 'elements' => [
['element' => 'th', 'properties' => [], 'content' => '$balance_due_label'],
['element' => 'th', 'properties' => [], 'content' => '$balance_due'],
['element' => 'th', 'properties' => [], 'content' => Number::formatMoney($this->invoices->sum('balance'), $this->entity->client)],
]],
];
}
@ -241,6 +241,10 @@ class Design extends BaseDesign
if ($this->entity instanceof Quote) {
$variables = $this->context['pdf_variables']['quote_details'];
if ($this->entity->partial > 0) {
$variables[] = '$quote.balance_due';
}
}
if ($this->entity instanceof Credit) {
@ -379,10 +383,10 @@ class Design extends BaseDesign
return [];
}
$outstanding = $this->invoices->sum('amount');
$outstanding = $this->invoices->sum('balance');
return [
['element' => 'p', 'content' => '$outstanding_label: $outstanding'],
['element' => 'p', 'content' => '$outstanding_label: ' . Number::formatMoney($outstanding, $this->entity->client)],
];
}
@ -424,7 +428,11 @@ class Design extends BaseDesign
public function statementPaymentTableTotals(): array
{
if ($this->type !== self::STATEMENT || !$this->payments->first()) {
if (is_null($this->payments) || !$this->payments->first() || $this->type !== self::STATEMENT) {
return [];
}
if (\array_key_exists('show_payments_table', $this->options) && $this->options['show_payments_table'] === false) {
return [];
}
@ -627,6 +635,10 @@ class Design extends BaseDesign
if (in_array('$outstanding', $variables)) {
$variables = \array_diff($variables, ['$outstanding']);
}
if ($this->entity->partial > 0) {
$variables[] = '$partial_due';
}
}
foreach (['discount'] as $property) {

View File

@ -40,8 +40,11 @@ trait DesignHelpers
if (isset($this->context['invoices'])) {
$this->invoices = $this->context['invoices'];
if (\count($this->invoices) >= 1) {
$this->entity = $this->invoices->first();
}
}
if (isset($this->context['payments'])) {
$this->payments = $this->context['payments'];
@ -68,6 +71,8 @@ trait DesignHelpers
$variables[$property] = $value;
}
$this->context['pdf_variables'] = $variables;
}
/**
@ -269,12 +274,21 @@ document.addEventListener('DOMContentLoaded', function() {
return false;
}
// Some variables don't map 1:1 to table columns. This gives us support for such cases.
$aliases = [
'$quote.balance_due' => 'partial',
];
try {
$_variable = explode('.', $variable)[1];
} catch (Exception $e) {
throw new Exception('Company settings seems to be broken. Missing $entity.variable type.');
}
if (\in_array($variable, \array_keys($aliases))) {
$_variable = $aliases[$variable];
}
if (is_null($this->entity->{$_variable})) {
return true;
}

View File

@ -82,6 +82,12 @@ class PdfMaker
return $this;
}
/**
* Final method to get compiled HTML.
*
* @param bool $final @deprecated
* @return mixed
*/
public function getCompiledHTML($final = false)
{
$html = $this->document->saveHTML();

View File

@ -97,6 +97,12 @@ class RecurringService
return $this;
}
public function fillDefaults()
{
return $this;
}
public function save()
{
$this->recurring_entity->save();

View File

@ -64,7 +64,8 @@ class RecurringInvoiceTransformer extends EntityTransformer
public function transform(RecurringInvoice $invoice)
{
return [
$data = [
'id' => $this->encodePrimaryKey($invoice->id),
'user_id' => $this->encodePrimaryKey($invoice->user_id),
'project_id' => $this->encodePrimaryKey($invoice->project_id),
@ -120,13 +121,19 @@ class RecurringInvoiceTransformer extends EntityTransformer
'entity_type' => 'recurringInvoice',
'frequency_id' => (string) $invoice->frequency_id,
'remaining_cycles' => (int) $invoice->remaining_cycles,
//'recurring_dates' => (array) $invoice->recurringDates(),
'recurring_dates' => [],
'auto_bill' => (string) $invoice->auto_bill,
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
'due_date_days' => (string) $invoice->due_date_days ?: '',
'paid_to_date' => (float) $invoice->paid_to_date,
'subscription_id' => (string)$this->encodePrimaryKey($invoice->subscription_id),
];
if(request()->has('show_dates') && request()->query('show_dates') == 'true')
$data['recurring_dates'] = (array) $invoice->recurringDates();
return $data;
}
}

View File

@ -14,6 +14,8 @@ namespace App\Utils;
use App\Models\Client;
use App\Utils\Traits\MakesDates;
use Carbon\Carbon;
use Illuminate\Support\Str;
use stdClass;
class Helpers
@ -97,4 +99,169 @@ class Helpers
return '';
}
/**
* Process reserved keywords on PDF.
*
* @param string $value
* @param Client $client
* @return null|string
*/
public static function processReservedKeywords(?string $value, Client $client): ?string
{
if(!$value)
return '';
Carbon::setLocale($client->locale());
$replacements = [
'literal' => [
':MONTH' => Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F'),
':YEAR' => now()->year,
':QUARTER' => 'Q' . now()->quarter,
':WEEK_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subDays(7)->translatedFormat($client->date_format()),
ctrans('texts.to'),
Carbon::now()->translatedFormat($client->date_format())
),
':WEEK_AHEAD' => \sprintf(
'%s %s %s',
Carbon::now()->addDays(7)->translatedFormat($client->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(14)->translatedFormat($client->date_format())
),
':WEEK' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($client->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(7)->translatedFormat($client->date_format())
),
],
'raw' => [
':MONTH' => now()->month,
':YEAR' => now()->year,
':QUARTER' => now()->quarter,
],
'ranges' => [
'MONTHYEAR' => Carbon::createFromDate(now()->year, now()->month),
],
'ranges_raw' => [
'MONTH' => now()->month,
'YEAR' => now()->year,
],
];
// First case, with ranges.
preg_match_all('/\[(.*?)]/', $value, $ranges);
$matches = array_shift($ranges);
foreach ($matches as $match) {
if (!Str::contains($match, '|')) {
continue;
}
if (Str::contains($match, '|')) {
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
$left = substr($parts[0], 1); // 'MONTH'
$right = substr($parts[1], 0, -1); // MONTH+2
// If left side is not part of replacements, skip.
if (!array_key_exists($left, $replacements['ranges'])) {
continue;
}
$_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
$_right = '';
// If right side doesn't have any calculations, replace with raw ranges keyword.
if (!Str::contains($right, ['-', '+', '/', '*'])) {
$_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
}
// If right side contains one of math operations, calculate.
if (Str::contains($right, ['+'])) {
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $right, $_matches);
$_operation = array_shift($_matches)[0]; // + -
$_value = explode($_operation, $right); // [MONTHYEAR, 4]
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y');
}
$replacement = sprintf('%s to %s', $_left, $_right);
$value = preg_replace(
sprintf('/%s/', preg_quote($match)), $replacement, $value, 1
);
}
}
// Second case with more common calculations.
preg_match_all('/:([^:\s]+)/', $value, $common);
$matches = array_shift($common);
foreach ($matches as $match) {
$matches = collect($replacements['literal'])->filter(function ($value, $key) use ($match) {
return Str::startsWith($match, $key);
});
if ($matches->count() === 0) {
continue;
}
if (!Str::contains($match, ['-', '+', '/', '*'])) {
$value = preg_replace(
sprintf('/%s/', $matches->keys()->first()), $replacements['literal'][$matches->keys()->first()], $value, 1
);
}
if (Str::contains($match, ['-', '+', '/', '*'])) {
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $match, $_matches);
$_operation = array_shift($_matches)[0];
$_value = explode($_operation, $match); // [:MONTH, 4]
$raw = strtr($matches->keys()->first(), $replacements['raw']); // :MONTH => 1
$number = $res = preg_replace("/[^0-9]/", '', $_value[1]); // :MONTH+1. || :MONTH+2! => 1 || 2
$target = "/{$matches->keys()->first()}\\{$_operation}{$number}/"; // /:$KEYWORD\\$OPERATION$VALUE => /:MONTH\\+1
$output = (int) $raw + (int)$_value[1];
if ($operation == '+') {
$output = (int) $raw + (int)$_value[1]; // 1 (:MONTH) + 4
}
if ($_operation == '-') {
$output = (int)$raw - (int)$_value[1]; // 1 (:MONTH) - 4
}
if ($_operation == '/' && (int)$_value[1] != 0) {
$output = (int)$raw / (int)$_value[1]; // 1 (:MONTH) / 4
}
if ($_operation == '*') {
$output = (int)$raw * (int)$_value[1]; // 1 (:MONTH) * 4
}
if ($matches->keys()->first() == ':MONTH') {
$output = \Carbon\Carbon::create()->month($output)->translatedFormat('F');
}
$value = preg_replace(
$target, $output, $value, 1
);
}
}
return $value;
}
}

View File

@ -18,7 +18,9 @@ use App\Models\InvoiceInvitation;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\transformTranslations;
use Exception;
use Illuminate\Support\Facades\App;
@ -96,6 +98,11 @@ class HtmlEngine
exit;
}
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($this->contact->preferredLocale());
$t->replace(Ninja::transformTranslations($this->settings));
$data = [];
$data['$global_margin'] = ['value' => '6.35mm', 'label' => ''];
$data['$tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
@ -127,11 +134,12 @@ class HtmlEngine
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$number_short'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number_short')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '', 'label' => ctrans('texts.invoice_terms')];
$data['$entity.terms'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->terms), $this->client) ?: '', 'label' => ctrans('texts.invoice_terms')];
$data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_invoice').'</a>', 'label' => ctrans('texts.view_invoice')];
$data['$viewLink'] = &$data['$view_link'];
$data['$viewButton'] = &$data['$view_link'];
$data['$view_button'] = &$data['$view_link'];
$data['$paymentButton'] = &$data['$view_link'];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.invoice_date')];
@ -231,12 +239,15 @@ class HtmlEngine
$data['$user.name'] = ['value' => $this->entity->user->present()->name(), 'label' => ctrans('texts.name')];
$data['$user.first_name'] = ['value' => $this->entity->user->first_name, 'label' => ctrans('texts.first_name')];
$data['$user.last_name'] = ['value' => $this->entity->user->last_name, 'label' => ctrans('texts.last_name')];
$data['$created_by_user'] = &$data['$user.name'];
$data['$assigned_to_user'] = ['value' => $this->entity->assigned_user ? $this->entity->assigned_user->present()->name() : '', 'label' => ctrans('texts.name')];
$data['$user_iban'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
$data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
$data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$invoice.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$invoice.public_notes'] = ['value' => $this->entity->public_notes ?: '', 'label' => ctrans('texts.public_notes')];
$data['$invoice.public_notes'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->public_notes), $this->client) ?: '', 'label' => ctrans('texts.public_notes')];
$data['$entity.public_notes'] = &$data['$invoice.public_notes'];
$data['$public_notes'] = &$data['$invoice.public_notes'];
$data['$notes'] = &$data['$public_notes'];
@ -434,7 +445,7 @@ class HtmlEngine
$data['$description'] = ['value' => '', 'label' => ctrans('texts.description')];
//$data['$entity_footer'] = ['value' => $this->client->getSetting("{$this->entity_string}_footer"), 'label' => ''];
$data['$entity_footer'] = ['value' => $this->entity->footer, 'label' => ''];
$data['$entity_footer'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->footer), $this->client), 'label' => ''];
$data['$page_size'] = ['value' => $this->settings->page_size, 'label' => ''];
$data['$page_layout'] = ['value' => property_exists($this->settings, 'page_layout') ? $this->settings->page_layout : 'Portrait', 'label' => ''];
@ -453,6 +464,9 @@ class HtmlEngine
$data['$payment.date'] = ['value' => '&nbsp;', 'label' => ctrans('texts.payment_date')];
$data['$method'] = ['value' => '&nbsp;', 'label' => ctrans('texts.method')];
$data['$statement_amount'] = ['value' => '', 'label' => ctrans('texts.amount')];
$data['$statement'] = ['value' => '', 'label' => ctrans('texts.statement')];
$arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data);

View File

@ -64,6 +64,9 @@ class Number
// remove everything except numbers and dot "."
$s = preg_replace("/[^0-9\.]/", '', $s);
if($s < 1)
return (float)$s;
// remove all seperators from first part and keep the end
$s = str_replace('.', '', substr($s, 0, -3)).substr($s, -3);

View File

@ -294,8 +294,8 @@ trait MakesInvoiceValues
$data[$key][$table_type.'.item'] = is_null(optional($item)->item) ? $item->product_key : $item->item;
$data[$key][$table_type.'.service'] = is_null(optional($item)->service) ? $item->product_key : $item->service;
$data[$key][$table_type.'.notes'] = $this->processReservedKeywords($item->notes);
$data[$key][$table_type.'.description'] = $this->processReservedKeywords($item->notes);
$data[$key][$table_type.'.notes'] = Helpers::processReservedKeywords($item->notes, $this->client);
$data[$key][$table_type.'.description'] = Helpers::processReservedKeywords($item->notes, $this->client);
/* need to test here as this is new - 18/09/2021*/
if(!array_key_exists($table_type.'.gross_line_total', $data[$key]))
@ -350,168 +350,6 @@ trait MakesInvoiceValues
return $data;
}
/**
* Process reserved words like :MONTH :YEAR :QUARTER
* as well as their operations.
*
* @param string $value
* @return string|null
*/
private function processReservedKeywords(string $value): ?string
{
Carbon::setLocale($this->client->locale());
$replacements = [
'literal' => [
':MONTH' => Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F'),
':YEAR' => now()->year,
':QUARTER' => 'Q' . now()->quarter,
':WEEK_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subDays(7)->translatedFormat($this->client->date_format()),
ctrans('texts.to'),
Carbon::now()->translatedFormat($this->client->date_format())
),
':WEEK_AHEAD' => \sprintf(
'%s %s %s',
Carbon::now()->addDays(7)->translatedFormat($this->client->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(14)->translatedFormat($this->client->date_format())
),
':WEEK' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($this->client->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(7)->translatedFormat($this->client->date_format())
),
],
'raw' => [
':MONTH' => now()->month,
':YEAR' => now()->year,
':QUARTER' => now()->quarter,
],
'ranges' => [
'MONTHYEAR' => Carbon::createFromDate(now()->year, now()->month),
],
'ranges_raw' => [
'MONTH' => now()->month,
'YEAR' => now()->year,
],
];
// First case, with ranges.
preg_match_all('/\[(.*?)]/', $value, $ranges);
$matches = array_shift($ranges);
foreach ($matches as $match) {
if (!Str::contains($match, '|')) {
continue;
}
if (Str::contains($match, '|')) {
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
$left = substr($parts[0], 1); // 'MONTH'
$right = substr($parts[1], 0, -1); // MONTH+2
// If left side is not part of replacements, skip.
if (!array_key_exists($left, $replacements['ranges'])) {
continue;
}
$_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
$_right = '';
// If right side doesn't have any calculations, replace with raw ranges keyword.
if (!Str::contains($right, ['-', '+', '/', '*'])) {
$_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
}
// If right side contains one of math operations, calculate.
if (Str::contains($right, ['+'])) {
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $right, $_matches);
$_operation = array_shift($_matches)[0]; // + -
$_value = explode($_operation, $right); // [MONTHYEAR, 4]
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y');
}
$replacement = sprintf('%s to %s', $_left, $_right);
$value = preg_replace(
sprintf('/%s/', preg_quote($match)), $replacement, $value, 1
);
}
}
// Second case with more common calculations.
preg_match_all('/:([^:\s]+)/', $value, $common);
$matches = array_shift($common);
foreach ($matches as $match) {
$matches = collect($replacements['literal'])->filter(function ($value, $key) use ($match) {
return Str::startsWith($match, $key);
});
if ($matches->count() === 0) {
continue;
}
if (!Str::contains($match, ['-', '+', '/', '*'])) {
$value = preg_replace(
sprintf('/%s/', $matches->keys()->first()), $replacements['literal'][$matches->keys()->first()], $value, 1
);
}
if (Str::contains($match, ['-', '+', '/', '*'])) {
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $match, $_matches);
$_operation = array_shift($_matches)[0];
$_value = explode($_operation, $match); // [:MONTH, 4]
$raw = strtr($matches->keys()->first(), $replacements['raw']); // :MONTH => 1
$number = $res = preg_replace("/[^0-9]/", '', $_value[1]); // :MONTH+1. || :MONTH+2! => 1 || 2
$target = "/{$matches->keys()->first()}\\{$_operation}{$number}/"; // /:$KEYWORD\\$OPERATION$VALUE => /:MONTH\\+1
$output = (int) $raw + (int)$_value[1];
if ($operation == '+') {
$output = (int) $raw + (int)$_value[1]; // 1 (:MONTH) + 4
}
if ($_operation == '-') {
$output = (int)$raw - (int)$_value[1]; // 1 (:MONTH) - 4
}
if ($_operation == '/' && (int)$_value[1] != 0) {
$output = (int)$raw / (int)$_value[1]; // 1 (:MONTH) / 4
}
if ($_operation == '*') {
$output = (int)$raw * (int)$_value[1]; // 1 (:MONTH) * 4
}
if ($matches->keys()->first() == ':MONTH') {
$output = \Carbon\Carbon::create()->month($output)->translatedFormat('F');
}
$value = preg_replace(
$target, $output, $value, 1
);
}
}
return $value;
}
/**
* Due to the way we are compiling the blade template we
* have no ability to iterate, so in the case

View File

@ -64,7 +64,7 @@
"league/flysystem-cached-adapter": "^1.1",
"league/fractal": "^0.17.0",
"league/omnipay": "^3.1",
"livewire/livewire": "^2.4",
"livewire/livewire": "^2.6",
"maennchen/zipstream-php": "^1.2",
"mollie/mollie-api-php": "^2.36",
"nwidart/laravel-modules": "^8.0",

115
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "54d84c4ecc41d25ece12b91b181e3431",
"content-hash": "96908a391244cbc96eefbb130bd7bed9",
"packages": [
{
"name": "apimatic/jsonmapper",
@ -323,16 +323,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.194.1",
"version": "3.194.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "67bdee05acef9e8ad60098090996690b49babd09"
"reference": "1f0a0cec5721b6346c968533fba9b44e462fc728"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/67bdee05acef9e8ad60098090996690b49babd09",
"reference": "67bdee05acef9e8ad60098090996690b49babd09",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1f0a0cec5721b6346c968533fba9b44e462fc728",
"reference": "1f0a0cec5721b6346c968533fba9b44e462fc728",
"shasum": ""
},
"require": {
@ -408,9 +408,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.194.1"
"source": "https://github.com/aws/aws-sdk-php/tree/3.194.2"
},
"time": "2021-09-17T18:15:42+00:00"
"time": "2021-09-21T18:14:06+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -2652,16 +2652,16 @@
},
{
"name": "google/apiclient",
"version": "v2.10.1",
"version": "v2.11.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client.git",
"reference": "11871e94006ce7a419bb6124d51b6f9ace3f679b"
"reference": "7db9eb40c8ba887e81c0fe84f2888a967396cdfb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/11871e94006ce7a419bb6124d51b6f9ace3f679b",
"reference": "11871e94006ce7a419bb6124d51b6f9ace3f679b",
"url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/7db9eb40c8ba887e81c0fe84f2888a967396cdfb",
"reference": "7db9eb40c8ba887e81c0fe84f2888a967396cdfb",
"shasum": ""
},
"require": {
@ -2669,8 +2669,8 @@
"google/apiclient-services": "~0.200",
"google/auth": "^1.10",
"guzzlehttp/guzzle": "~5.3.3||~6.0||~7.0",
"guzzlehttp/psr7": "^1.2",
"monolog/monolog": "^1.17|^2.0",
"guzzlehttp/psr7": "^1.7||^2.0.0",
"monolog/monolog": "^1.17||^2.0",
"php": "^5.6|^7.0|^8.0",
"phpseclib/phpseclib": "~2.0||^3.0.2"
},
@ -2679,10 +2679,12 @@
"composer/composer": "^1.10.22",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7",
"phpcompatibility/php-compatibility": "^9.2",
"phpunit/phpunit": "^5.7||^8.5.13",
"phpspec/prophecy-phpunit": "^1.1||^2.0",
"phpunit/phpunit": "^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "~2.3",
"symfony/css-selector": "~2.1",
"symfony/dom-crawler": "~2.1"
"symfony/dom-crawler": "~2.1",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)"
@ -2715,22 +2717,22 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client/issues",
"source": "https://github.com/googleapis/google-api-php-client/tree/v2.10.1"
"source": "https://github.com/googleapis/google-api-php-client/tree/v2.11.0"
},
"time": "2021-06-25T14:25:44+00:00"
"time": "2021-09-20T21:15:55+00:00"
},
{
"name": "google/apiclient-services",
"version": "v0.212.0",
"version": "v0.213.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "2c4bd512502ad9cdfec8ea711ea1592c79d345e5"
"reference": "260311821505438eb9208b068da0d849b8ea9baa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/2c4bd512502ad9cdfec8ea711ea1592c79d345e5",
"reference": "2c4bd512502ad9cdfec8ea711ea1592c79d345e5",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/260311821505438eb9208b068da0d849b8ea9baa",
"reference": "260311821505438eb9208b068da0d849b8ea9baa",
"shasum": ""
},
"require": {
@ -2759,9 +2761,9 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.212.0"
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.213.0"
},
"time": "2021-09-12T11:18:27+00:00"
"time": "2021-09-19T11:18:26+00:00"
},
{
"name": "google/auth",
@ -5586,20 +5588,20 @@
},
{
"name": "nette/utils",
"version": "v3.2.3",
"version": "v3.2.5",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "5c36cc1ba9bb6abb8a9e425cf054e0c3fd5b9822"
"reference": "9cd80396ca58d7969ab44fc7afcf03624dfa526e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/5c36cc1ba9bb6abb8a9e425cf054e0c3fd5b9822",
"reference": "5c36cc1ba9bb6abb8a9e425cf054e0c3fd5b9822",
"url": "https://api.github.com/repos/nette/utils/zipball/9cd80396ca58d7969ab44fc7afcf03624dfa526e",
"reference": "9cd80396ca58d7969ab44fc7afcf03624dfa526e",
"shasum": ""
},
"require": {
"php": ">=7.2 <8.1"
"php": ">=7.2 <8.2"
},
"conflict": {
"nette/di": "<3.0.6"
@ -5665,22 +5667,22 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v3.2.3"
"source": "https://github.com/nette/utils/tree/v3.2.5"
},
"time": "2021-08-16T21:05:00+00:00"
"time": "2021-09-20T10:50:11+00:00"
},
{
"name": "nikic/php-parser",
"version": "v4.12.0",
"version": "v4.13.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "6608f01670c3cc5079e18c1dab1104e002579143"
"reference": "50953a2691a922aa1769461637869a0a2faa3f53"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6608f01670c3cc5079e18c1dab1104e002579143",
"reference": "6608f01670c3cc5079e18c1dab1104e002579143",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50953a2691a922aa1769461637869a0a2faa3f53",
"reference": "50953a2691a922aa1769461637869a0a2faa3f53",
"shasum": ""
},
"require": {
@ -5721,9 +5723,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v4.13.0"
},
"time": "2021-07-21T10:44:31+00:00"
"time": "2021-09-20T12:20:58+00:00"
},
{
"name": "nwidart/laravel-modules",
@ -12740,16 +12742,16 @@
},
{
"name": "filp/whoops",
"version": "2.14.1",
"version": "2.14.3",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "15ead64e9828f0fc90932114429c4f7923570cb1"
"reference": "89584ce67dd32307f1063cc43846674f4679feda"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/15ead64e9828f0fc90932114429c4f7923570cb1",
"reference": "15ead64e9828f0fc90932114429c4f7923570cb1",
"url": "https://api.github.com/repos/filp/whoops/zipball/89584ce67dd32307f1063cc43846674f4679feda",
"reference": "89584ce67dd32307f1063cc43846674f4679feda",
"shasum": ""
},
"require": {
@ -12799,7 +12801,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
"source": "https://github.com/filp/whoops/tree/2.14.1"
"source": "https://github.com/filp/whoops/tree/2.14.3"
},
"funding": [
{
@ -12807,7 +12809,7 @@
"type": "github"
}
],
"time": "2021-08-29T12:00:00+00:00"
"time": "2021-09-19T12:00:00+00:00"
},
{
"name": "friendsofphp/php-cs-fixer",
@ -13290,33 +13292,32 @@
},
{
"name": "nunomaduro/collision",
"version": "v5.9.0",
"version": "v5.10.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
"reference": "63456f5c3e8c4bc52bd573e5c85674d64d84fd43"
"reference": "3004cfa49c022183395eabc6d0e5207dfe498d00"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/63456f5c3e8c4bc52bd573e5c85674d64d84fd43",
"reference": "63456f5c3e8c4bc52bd573e5c85674d64d84fd43",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/3004cfa49c022183395eabc6d0e5207dfe498d00",
"reference": "3004cfa49c022183395eabc6d0e5207dfe498d00",
"shasum": ""
},
"require": {
"facade/ignition-contracts": "^1.0",
"filp/whoops": "^2.7.2",
"filp/whoops": "^2.14.3",
"php": "^7.3 || ^8.0",
"symfony/console": "^5.0"
},
"require-dev": {
"brianium/paratest": "^6.1",
"fideloper/proxy": "^4.4.1",
"friendsofphp/php-cs-fixer": "^3.0",
"fruitcake/laravel-cors": "^2.0.3",
"laravel/framework": "^8.0 || ^9.0",
"laravel/framework": "8.x-dev",
"nunomaduro/larastan": "^0.6.2",
"nunomaduro/mock-final-classes": "^1.0",
"orchestra/testbench": "^6.0 || ^7.0",
"orchestra/testbench": "^6.0",
"phpstan/phpstan": "^0.12.64",
"phpunit/phpunit": "^9.5.0"
},
@ -13374,7 +13375,7 @@
"type": "patreon"
}
],
"time": "2021-08-26T15:32:09+00:00"
"time": "2021-09-20T15:06:32+00:00"
},
{
"name": "openlss/lib-array2xml",
@ -15274,16 +15275,16 @@
},
{
"name": "swagger-api/swagger-ui",
"version": "v3.52.2",
"version": "v3.52.3",
"source": {
"type": "git",
"url": "https://github.com/swagger-api/swagger-ui.git",
"reference": "e5611d72ff6b4affb373fa8859cc5feb6981f367"
"reference": "aa9f2e6733327b5f042f2529db76558d9c09bed2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/e5611d72ff6b4affb373fa8859cc5feb6981f367",
"reference": "e5611d72ff6b4affb373fa8859cc5feb6981f367",
"url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/aa9f2e6733327b5f042f2529db76558d9c09bed2",
"reference": "aa9f2e6733327b5f042f2529db76558d9c09bed2",
"shasum": ""
},
"type": "library",
@ -15329,9 +15330,9 @@
],
"support": {
"issues": "https://github.com/swagger-api/swagger-ui/issues",
"source": "https://github.com/swagger-api/swagger-ui/tree/v3.52.2"
"source": "https://github.com/swagger-api/swagger-ui/tree/v3.52.3"
},
"time": "2021-09-13T12:46:28+00:00"
"time": "2021-09-20T12:12:56+00:00"
},
{
"name": "symfony/debug",

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.3.16',
'app_tag' => '5.3.16',
'app_version' => '5.3.17',
'app_tag' => '5.3.17',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),
@ -37,7 +37,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://9b4e15e575214354a7d666489783904a@sentry.invoicing.co/6'),
'sentry_dsn' => env('SENTRY_LARAVEL_DSN', 'https://32f01ea994744fa08a0f688769cef78a@sentry.invoicing.co/9'),
'environment' => env('NINJA_ENVIRONMENT', 'selfhost'), // 'hosted', 'development', 'selfhost', 'reseller'
'preconfigured_install' => env('PRECONFIGURED_INSTALL',false),
'update_secret' => env('UPDATE_SECRET', ''),

View File

@ -0,0 +1,52 @@
<?php
use App\Models\Credit;
use App\Models\Gateway;
use App\Models\Invoice;
use App\Models\Quote;
use App\Utils\Ninja;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class SetSquareTestModeBoolean extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
//
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//Fixes a state where the deleted_at timestamp is 000
if(!Ninja::isHosted())
{
Invoice::withTrashed()->where('deleted_at', '0000-00-00 00:00:00.000000')->update(['deleted_at' => null]);
Quote::withTrashed()->where('deleted_at', '0000-00-00 00:00:00.000000')->update(['deleted_at' => null]);
Credit::withTrashed()->where('deleted_at', '0000-00-00 00:00:00.000000')->update(['deleted_at' => null]);
}
// fixes a bool cast to string back to bool
if($gateway = Gateway::find(57))
{
$fields = json_decode($gateway->fields);
$fields->testMode = false;
$gateway->fields = json_encode($fields);
$gateway->save();
}
}
}

View File

@ -0,0 +1,60 @@
<?php
use App\Models\Currency;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Eloquent\Model;
class AddCurrencies extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Model::unguard();
$currencies = [
['id' => 105, 'name' => 'Gambia Dalasi', 'code' => 'GMD', 'symbol' => 'D', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 106, 'name' => 'Paraguayan Guarani', 'code' => 'PYG', 'symbol' => '₲', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 107, 'name' => 'Malawi Kwacha','code' => 'MWK', 'symbol' => 'MK', 'precision' => '2','thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 108, 'name' => 'Zimbabwean Dollar', 'code' => 'ZWL', 'symbol' => 'Z$', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 109, 'name' => 'Cambodian Riel', 'code' => 'KHR', 'symbol' => '៛', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 110, 'name' => 'Vanuatu Vatu','code' => 'VUV', 'symbol' => 'VT', 'precision' => '0','thousand_separator' => ',','decimal_separator' => '.'],
];
foreach ($currencies as $currency) {
$record = Currency::whereCode($currency['code'])->first();
if ($record) {
$record->name = $currency['name'];
$record->symbol = $currency['symbol'];
$record->precision = $currency['precision'];
$record->thousand_separator = $currency['thousand_separator'];
$record->decimal_separator = $currency['decimal_separator'];
if (isset($currency['swap_currency_symbol'])) {
$record->swap_currency_symbol = $currency['swap_currency_symbol'];
}
$record->save();
} else {
Currency::create($currency);
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@ -0,0 +1,24 @@
<?php
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Database\Migrations\Migration;
class AddMollieBankTransferToPaymentTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$type = new PaymentType();
$type->id = 34;
$type->name = 'Mollie Bank Transfer';
$type->gateway_type_id = GatewayType::BANK_TRANSFER;
$type->save();
}
}

View File

@ -80,7 +80,7 @@ class PaymentLibrariesSeeder extends Seeder
['id' => 53, 'name' => 'PagSeguro', 'provider' => 'PagSeguro', 'key' => 'ef498756b54db63c143af0ec433da803', 'fields' => '{"email":"","token":"","sandbox":false}'],
['id' => 54, 'name' => 'PAYMILL', 'provider' => 'Paymill', 'key' => 'ca52f618a39367a4c944098ebf977e1c', 'fields' => '{"apiKey":""}'],
['id' => 55, 'name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 21, 'key' => '54faab2ab6e3223dbe848b1686490baa', 'fields' => '{"name":"","text":""}'],
['id' => 57, 'name' => 'Square', 'provider' => 'Square', 'is_offsite' => false, 'sort_order' => 21, 'key' => '65faab2ab6e3223dbe848b1686490baz', 'fields' => '{"accessToken":"","applicationId":"","locationId":"","testMode":"false"}'],
['id' => 57, 'name' => 'Square', 'provider' => 'Square', 'is_offsite' => false, 'sort_order' => 21, 'key' => '65faab2ab6e3223dbe848b1686490baz', 'fields' => '{"accessToken":"","applicationId":"","locationId":"","testMode":false}'],
];
foreach ($gateways as $gateway) {
@ -97,7 +97,7 @@ class PaymentLibrariesSeeder extends Seeder
Gateway::query()->update(['visible' => 0]);
Gateway::whereIn('id', [1,7,15,20,39,46,55,50,57])->update(['visible' => 1]);
Gateway::whereIn('id', [1,7,11,15,20,39,46,55,50,57])->update(['visible' => 1]);
if (Ninja::isHosted()) {
Gateway::whereIn('id', [20])->update(['visible' => 0]);

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"/": "fd698ff0c4251992cffae325bd810e94",
"/": "d54a2d8f5df9a52b1936136260e327b3",
"assets/NOTICES": "9eb7e2eb2888ea5bae5f536720db37cd",
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
@ -34,7 +34,7 @@ const RESOURCES = {
"favicon.ico": "51636d3a390451561744c42188ccd628",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"main.dart.js": "8fc88d4b5a3256d5f9ff5c0f1bdeb92b"
"main.dart.js": "d0bf26e6fb4226713654b97cdfef6a53"
};
// The application shell files that are downloaded before a service worker can

View File

@ -1,2 +1,2 @@
/*! For license information please see stripe-sofort.js.LICENSE.txt */
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=6)}({6:function(e,t,n){e.exports=n("RFiP")},RFiP:function(e,t){var n,r,o,u;function c(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var i=null!==(n=null===(r=document.querySelector('meta[name="stripe-publishable-key"]'))||void 0===r?void 0:r.content)&&void 0!==n?n:"",a=null!==(o=null===(u=document.querySelector('meta[name="stripe-account-id"]'))||void 0===u?void 0:u.content)&&void 0!==o?o:"";new function e(t,n){var r=this;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),c(this,"setupStripe",(function(){return r.stripe=Stripe(r.key),r.stripeConnect&&(r.stripe.stripeAccount=a),r})),c(this,"handle",(function(){var e={type:"sofort",customer:document.querySelector('meta[name="customer"]').content,amount:document.querySelector('meta[name="amount"]').content,currency:"eur",redirect:{return_url:document.querySelector('meta[name="return-url"]').content},sofort:{country:document.querySelector('meta[name="country"]').content}};document.getElementById("pay-now").addEventListener("click",(function(t){document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),r.stripe.createSource(e).then((function(e){if(e.hasOwnProperty("source"))return window.location=e.source.redirect.url;document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden"),this.errors.textContent="",this.errors.textContent=e.error.message,this.errors.hidden=!1,document.getElementById("pay-now").disabled=!1}))}))})),this.key=t,this.errors=document.getElementById("errors"),this.stripeConnect=n}(i,a).setupStripe().handle()}});
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=6)}({6:function(e,t,n){e.exports=n("RFiP")},RFiP:function(e,t){var n,r,o,u;function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var c=null!==(n=null===(r=document.querySelector('meta[name="stripe-publishable-key"]'))||void 0===r?void 0:r.content)&&void 0!==n?n:"",l=null!==(o=null===(u=document.querySelector('meta[name="stripe-account-id"]'))||void 0===u?void 0:u.content)&&void 0!==o?o:"";new function e(t,n){var r=this;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),i(this,"setupStripe",(function(){return r.stripe=Stripe(r.key),r.stripeConnect&&(r.stripe.stripeAccount=l),r})),i(this,"handle",(function(){document.getElementById("pay-now").addEventListener("click",(function(e){document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),r.stripe.confirmSofortPayment(document.querySelector("meta[name=pi-client-secret").content,{payment_method:{sofort:{country:document.querySelector('meta[name="country"]').content}},return_url:document.querySelector('meta[name="return-url"]').content})}))})),this.key=t,this.errors=document.getElementById("errors"),this.stripeConnect=n}(c,l).setupStripe().handle()}});

File diff suppressed because one or more lines are too long

2
public/js/clients/statements/view.js vendored Normal file
View File

@ -0,0 +1,2 @@
/*! For license information please see view.js.LICENSE.txt */
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var a=t[r]={i:r,l:!1,exports:{}};return e[r].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(r,a,function(t){return e[t]}.bind(null,a));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=26)}({26:function(e,t,n){e.exports=n("LcZE")},LcZE:function(e,t){function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.url=new URL(document.querySelector("meta[name=pdf-url]").content),this.startDate="",this.endDate="",this.showPaymentsTable=!1,this.showAgingTable=!1}var t,r,a;return t=e,(r=[{key:"bindEventListeners",value:function(){var e=this;["#date-from","#date-to","#show-payments-table","#show-aging-table"].forEach((function(t){document.querySelector(t).addEventListener("change",(function(t){return e.handleValueChange(t)}))}))}},{key:"handleValueChange",value:function(e){"checkbox"===e.target.type?this[e.target.dataset.field]=e.target.checked:this[e.target.dataset.field]=e.target.value,this.updatePdf()}},{key:"composedUrl",get:function(){return this.url.search="",this.startDate.length>0&&this.url.searchParams.append("start_date",this.startDate),this.endDate.length>0&&this.url.searchParams.append("end_date",this.endDate),this.url.searchParams.append("show_payments_table",+this.showPaymentsTable),this.url.searchParams.append("show_aging_table",+this.showAgingTable),this.url.href}},{key:"updatePdf",value:function(){document.querySelector("meta[name=pdf-url]").content=this.composedUrl;var e=document.querySelector("#pdf-iframe");e&&(e.src=this.composedUrl),document.querySelector("meta[name=pdf-url]").dispatchEvent(new Event("change"))}},{key:"handle",value:function(){var e=this;this.bindEventListeners(),document.querySelector("#pdf-download").addEventListener("click",(function(){var t=new URL(e.composedUrl);t.searchParams.append("download",1),window.location.href=t.href}))}}])&&n(t.prototype,r),a&&n(t,a),e}())).handle()}});

View File

@ -0,0 +1,9 @@
/**
* 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
*/

149609
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

151923
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

154622
public/main.last.dart.js vendored

File diff suppressed because one or more lines are too long

151087
public/main.next.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

151339
public/main.wasm.dart.js vendored

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",
"/css/app.css": "/css/app.css?id=46021f35ee55aca9ff20",
"/css/app.css": "/css/app.css?id=08bae341ed680d6ba544",
"/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/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
@ -19,12 +19,13 @@
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=81c2623fc1e5769b51c7",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=665ddf663500767f1a17",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=a30464874dee84678344",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=df63bd9e9837a420fd5d",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=231571942310348aa616",
"/js/clients/payments/wepay-credit-card.js": "/js/clients/payments/wepay-credit-card.js?id=f51400e03c5fdb6cdabe",
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=1b8f9325aa6e8595e7fa",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=85bcae0a646882e56b12",
"/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=2a99d83305ba87bfa6cc",
"/js/clients/statements/view.js": "/js/clients/statements/view.js?id=ca3ec4cea0de824f3a36",
"/js/setup/setup.js": "/js/setup/setup.js?id=8d454e7090f119552a6c",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad"
}

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 +1 @@
{"/livewire.js":"/livewire.js?id=b09cb328e689f1bb8d77"}
{"/livewire.js":"/livewire.js?id=21fa1dd78491a49255cd"}

View File

@ -25,40 +25,26 @@ class ProcessSOFORT {
};
handle = () => {
let data = {
type: 'sofort',
customer: document.querySelector('meta[name="customer"]').content,
amount: document.querySelector('meta[name="amount"]').content,
currency: 'eur',
redirect: {
return_url: document.querySelector('meta[name="return-url"]')
.content,
},
sofort: {
country: document.querySelector('meta[name="country"]').content,
},
};
document.getElementById('pay-now').addEventListener('click', (e) => {
document.getElementById('pay-now').disabled = true;
document.querySelector('#pay-now > svg').classList.remove('hidden');
document.querySelector('#pay-now > span').classList.add('hidden');
this.stripe.createSource(data).then(function(result) {
if (result.hasOwnProperty('source')) {
return (window.location = result.source.redirect.url);
this.stripe.confirmSofortPayment(
document.querySelector('meta[name=pi-client-secret').content,
{
payment_method: {
sofort: {
country: document.querySelector(
'meta[name="country"]'
).content,
},
},
return_url: document.querySelector(
'meta[name="return-url"]'
).content,
}
document.getElementById('pay-now').disabled = false;
document.querySelector('#pay-now > svg').classList.add('hidden');
document.querySelector('#pay-now > span').classList.remove('hidden');
this.errors.textContent = '';
this.errors.textContent = result.error.message;
this.errors.hidden = false;
document.getElementById('pay-now').disabled = false;
});
);
});
};
}

View File

@ -80,6 +80,15 @@ class PDF {
.getElementById('zoom-out')
.addEventListener('click', () => this.handleZoomChange());
document
.querySelector('meta[name=pdf-url]')
.addEventListener('change', () => {
this.canvas.getContext('2d').clearRect(0, 0, this.canvas.width, this.canvas.height);
this.url = document.querySelector("meta[name='pdf-url']").content;
this.handle();
})
return this;
}

95
resources/js/clients/statements/view.js vendored Normal file
View File

@ -0,0 +1,95 @@
/**
* 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
*/
class Statement {
constructor() {
this.url = new URL(
document.querySelector('meta[name=pdf-url]').content
);
this.startDate = '';
this.endDate = '';
this.showPaymentsTable = false;
this.showAgingTable = false;
}
bindEventListeners() {
[
'#date-from',
'#date-to',
'#show-payments-table',
'#show-aging-table',
].forEach((selector) => {
document
.querySelector(selector)
.addEventListener('change', (event) =>
this.handleValueChange(event)
);
});
}
handleValueChange(event) {
if (event.target.type === 'checkbox') {
this[event.target.dataset.field] = event.target.checked;
} else {
this[event.target.dataset.field] = event.target.value;
}
this.updatePdf();
}
get composedUrl() {
this.url.search = '';
if (this.startDate.length > 0) {
this.url.searchParams.append('start_date', this.startDate);
}
if (this.endDate.length > 0) {
this.url.searchParams.append('end_date', this.endDate);
}
this.url.searchParams.append(
'show_payments_table',
+this.showPaymentsTable
);
this.url.searchParams.append('show_aging_table', +this.showAgingTable);
return this.url.href;
}
updatePdf() {
document.querySelector('meta[name=pdf-url]').content = this.composedUrl;
let iframe = document.querySelector('#pdf-iframe');
if (iframe) {
iframe.src = this.composedUrl;
}
document
.querySelector('meta[name=pdf-url]')
.dispatchEvent(new Event('change'));
}
handle() {
this.bindEventListeners();
document
.querySelector('#pdf-download')
.addEventListener('click', () => {
let url = new URL(this.composedUrl);
url.searchParams.append('download', 1);
window.location.href = url.href;
})
}
}
new Statement().handle();

View File

@ -1779,6 +1779,7 @@ $LANG = array(
'lang_Bulgarian' => 'Bulgarian',
'lang_Russian (Russia)' => 'Russian (Russia)',
// Industries
'industry_Accounting & Legal' => 'Accounting & Legal',
'industry_Advertising' => 'Advertising',
@ -2471,6 +2472,13 @@ $LANG = array(
'currency_kazakhstani_tenge' => 'Kazakhstani Tenge',
'currency_gibraltar_pound' => 'Gibraltar Pound',
'currency_gambia_dalasi' => 'Gambia Dalasi',
'currency_paraguayan_guarani' => 'Paraguayan Guarani',
'currency_malawi_kwacha' => 'Malawi Kwacha',
'currency_zimbabwean_dollar' => 'Zimbabwean Dollar',
'currency_cambodian_riel' => 'Cambodian Riel',
'currency_vanuatu_vatu' => 'Vanuatu Vatu',
'review_app_help' => 'We hope you\'re enjoying using the app.<br/>If you\'d consider :link we\'d greatly appreciate it!',
'writing_a_review' => 'writing a review',
@ -4305,6 +4313,7 @@ $LANG = array(
'unable_to_verify_payment_method' => 'Unable to verify payment method.',
'generic_gateway_error' => 'Gateway configuration error. Please check your credentials.',
'my_documents' => 'My documents',
'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.',
);
return $LANG;

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}" data-build="{{ $build }}" user-agent="{{ $user_agent }}">
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}" data-user-agent="{{ $user_agent }}">
<head>
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Version: {{ config('ninja.app_version') }} -->

View File

@ -257,6 +257,11 @@
max-height: 160px;
}
#statement-invoice-table-totals > p {
margin-right: 2rem;
margin-top: 1rem;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -345,6 +345,12 @@
? document.getElementById(tableIdentifier).style.display = 'none'
: '';
});
// If we have elements in these tables, we can change label to "Statement" & hide entity details.
if (document.querySelectorAll('#statement-payment-table > tbody, #statement-payment-table > tbody, #statement-aging-table-totals > tbody').length > 0) {
document.querySelector('.entity-label').innerText = '$statement_label';
document.querySelector('.entity-details-wrapper').style.display = 'none';
}
});
</script>
</div>

View File

@ -54,8 +54,12 @@
height: 120px;
}
.company-logo {
height: 100%;
.company-logo-wrapper {
padding-bottom: 60px;
}
.company-logo-wrapper {
height: 5rem;
}
.header-invoice-number {
@ -266,8 +270,10 @@
</div>
<div class="logo-and-partial-entity-info">
<div class="company-logo-wrapper">
<img class="company-logo" src="$company.logo"
alt="$company.name logo">
</div>
<div class="top-right-side-section">
<div dir="$dir">
<span class="header-payment-due-label">$payment_due_label:</span>

View File

@ -77,7 +77,7 @@
</div>
@endif
@if(!empty($company->present()->website()))
@if(!is_null($company) && !empty($company->present()->website()))
<div class="mt-5 text-center">
<a class="button-link text-sm" href="{{ $company->present()->website() }}">
{{ ctrans('texts.back_to', ['url' => parse_url($company->present()->website())['host'] ?? $company->present()->website() ]) }}

View File

@ -10,7 +10,7 @@
alt="Background image">
</div>
@endif
<div class="col-span-2 h-screen flex">
<div class="{{ $account && !$account->isPaid() ? 'col-span-2' : 'col-span-3' }} h-screen flex">
<div class="m-auto w-1/2 md:w-1/3 lg:w-1/4">
@if($account && !$account->isPaid())
<div>

View File

@ -11,7 +11,7 @@
</div>
@endif
<div class="col-span-2 h-screen flex">
<div class="{{ $account && !$account->isPaid() ? 'col-span-2' : 'col-span-3' }} h-screen flex">
<div class="m-auto w-1/2 md:w-1/3 lg:w-1/4">
@if($account && !$account->isPaid())

View File

@ -69,19 +69,6 @@
</div>
</div>
@if(auth('contact')->user())
<a href="{{ route('client.invoices.index') }}" class="block mt-16 inline-flex items-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-arrow-left">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
<span>{{ ctrans('texts.client_portal') }}</span>
</a>
@endif
@if($subscription->service()->getPlans()->count() > 1)
<div class="flex flex-col mt-10">
<p class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">

View File

@ -3,7 +3,7 @@
@endphp
@push('head')
<meta name="pdf-url" content="{{ $entity->pdf_file_path(null, 'url', true) }}">
<meta name="pdf-url" content="{{ $url ?? $entity->pdf_file_path(null, 'url', true) }}">
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
@endpush
@ -72,7 +72,7 @@
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs">
<div class="py-1">
<a target="_blank" href="?mode=fullscreen"
<a target="_blank" href="{{ $fullscreen_url ?? '?mode=fullscreen' }}"
class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
</div>
</div>
@ -86,7 +86,7 @@
<canvas id="pdf-placeholder" class="shadow rounded-lg bg-white mt-4 p-4"></canvas>
</div>
@else
<iframe src="{{ $entity->pdf_file_path(null, 'url', true) }}" class="h-screen w-full border-0 mt-4"></iframe>
<iframe id="pdf-iframe" src="{{ $url ?? $entity->pdf_file_path(null, 'url', true) }}" class="h-screen w-full border-0 mt-4"></iframe>
@endif

View File

@ -0,0 +1,8 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.bank_transfer'), 'card_title' =>
ctrans('texts.bank_transfer')])
@section('gateway_content')
@component('portal.ninja2020.components.general.card-element-single')
{{ __('texts.payment_method_cannot_be_preauthorized') }}
@endcomponent
@endsection

View File

@ -7,6 +7,7 @@
<meta name="amount" content="{{ $stripe_amount }}">
<meta name="country" content="{{ $country }}">
<meta name="customer" content="{{ $customer }}">
<meta name="pi-client-secret" content="{{ $pi_client_secret }}">
@endsection
@section('gateway_content')

View File

@ -0,0 +1,42 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.statement'))
@push('head')
<meta name="pdf-url" content="{{ route('client.statement.raw') }}">
@endpush
@section('body')
<div class="flex flex-col md:flex-row md:justify-between">
<div class="flex flex-col md:flex-row md:items-center">
<div class="flex">
<label for="from" class="block w-full flex items-center mr-4">
<span class="mr-2">{{ ctrans('texts.from') }}:</span>
<input id="date-from" type="date" class="input w-full" data-field="startDate" value="{{ now()->startOfYear()->format('Y-m-d') }}">
</label>
<label for="to" class="block w-full flex items-center mr-4">
<span class="mr-2">{{ ctrans('texts.to') }}:</span>
<input id="date-to" type="date" class="input w-full" data-field="endDate" value="{{ now()->format('Y-m-d') }}">
</label>
</div> <!-- End date range -->
<label for="show_payments" class="block flex items-center mr-4 mt-4 md:mt-0">
<input id="show-payments-table" type="checkbox" data-field="showPaymentsTable" class="form-checkbox" autocomplete="off">
<span class="ml-2">{{ ctrans('texts.show_payments') }}</span>
</label> <!-- End show payments checkbox -->
<label for="show_aging" class="block flex items-center">
<input id="show-aging-table" type="checkbox" data-field="showAgingTable" class="form-checkbox" autocomplete="off">
<span class="ml-2">{{ ctrans('texts.show_aging') }}</span>
</label> <!-- End show aging checkbox -->
</div>
<button id="pdf-download" class="button button-primary bg-primary mt-4 md:mt-0">{{ ctrans('texts.download') }}</button>
</div>
@include('portal.ninja2020.components.pdf-viewer', ['url' => route('client.statement.raw')])
@endsection
@push('footer')
<script src="{{ asset('js/clients/statements/view.js') }}"></script>
@endpush

View File

@ -80,6 +80,9 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
Route::resource('tasks', 'ClientPortal\TaskController')->only(['index']);
Route::get('statement', 'ClientPortal\StatementController@index')->name('statement');
Route::get('statement/raw', 'ClientPortal\StatementController@raw')->name('statement.raw');
Route::post('upload', 'ClientPortal\UploadController')->name('upload.store');
Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');

View File

@ -0,0 +1,72 @@
<?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\Browser\ClientPortal\Gateways\Mollie;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\ClientPortal\Login;
class BankTransferTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', '1bd651fb213ca0c9d66ae3c336dc77e8')->restore();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testSuccessfulPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Bank Transfer')
->waitForText('Test profile')
->radio('final_state', 'paid')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Completed');
});
}
public function testPendingPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Bank Transfer')
->waitForText('Test profile')
->radio('final_state', 'open')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Pending');
});
}
}

View File

@ -12,6 +12,7 @@
namespace Tests\Browser\ClientPortal\Gateways\Mollie;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
@ -26,9 +27,9 @@ class CreditCardTest extends DuskTestCase
$browser->driver->manage()->deleteAllCookies();
}
// $this->disableCompanyGateways();
$this->disableCompanyGateways();
// CompanyGateway::where('gateway_key', '3758e7f7c6f4cecf0f4f348b9a00f456')->restore();
CompanyGateway::where('gateway_key', '1bd651fb213ca0c9d66ae3c336dc77e8')->restore();
$this->browse(function (Browser $browser) {
$browser

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