Merge pull request #6785 from turbo124/v5-stable

v5.3.21
This commit is contained in:
David Bomba 2021-10-06 18:04:30 +11:00 committed by GitHub
commit c3ab4aa0b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 288259 additions and 287277 deletions

View File

@ -1 +1 @@
5.3.19
5.3.21

View File

@ -28,9 +28,9 @@ class CloneQuoteToInvoiceFactory
unset($quote_array['invoice_id']);
unset($quote_array['id']);
unset($quote_array['invitations']);
unset($quote_array['terms']);
// unset($quote_array['public_notes']);
unset($quote_array['footer']);
//unset($quote_array['terms']);
//unset($quote_array['public_notes']);
//unset($quote_array['footer']);
unset($quote_array['design_id']);
foreach ($quote_array as $key => $value) {

View File

@ -19,7 +19,10 @@ use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Services\ClientPortal\InstantPayment;
use App\Utils\CurlUtils;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
@ -65,6 +68,9 @@ class InvitationController extends Controller
private function genericRouter(string $entity, string $invitation_key)
{
if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
return response()->json(['message' => 'Invalid resource request']);
$key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
@ -133,6 +139,9 @@ class InvitationController extends Controller
private function returnRawPdf(string $entity, string $invitation_key)
{
if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
return response()->json(['message' => 'Invalid resource request']);
$key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
@ -181,4 +190,41 @@ class InvitationController extends Controller
return redirect()->route('client.payments.show', $payment->hashed_id);
}
public function payInvoice(Request $request, string $invitation_key)
{
$invitation = InvoiceInvitation::where('key', $invitation_key)
->with('contact.client')
->firstOrFail();
auth()->guard('contact')->login($invitation->contact, true);
$invoice = $invitation->invoice;
if($invoice->partial > 0)
$amount = round($invoice->partial, (int)$invoice->client->currency()->precision);
else
$amount = round($invoice->balance, (int)$invoice->client->currency()->precision);
$gateways = $invitation->contact->client->service()->getPaymentMethods($amount);
if(is_array($gateways))
{
$data = [
'company_gateway_id' => $gateways[0]['company_gateway_id'],
'payment_method_id' => $gateways[0]['gateway_type_id'],
'payable_invoices' => [
['invoice_id' => $invitation->invoice->hashed_id, 'amount' => $amount],
],
'signature' => false
];
$request->replace($data);
return (new InstantPayment($request))->run();
}
abort(404, "Invoice not found");
}
}

View File

@ -23,6 +23,7 @@ use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\Services\ClientPortal\InstantPayment;
use App\Services\Subscription\SubscriptionService;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
@ -79,235 +80,7 @@ class PaymentController extends Controller
*/
public function process(Request $request)
{
$is_credit_payment = false;
$tokens = [];
if ($request->input('company_gateway_id') == CompanyGateway::GATEWAY_CREDIT) {
$is_credit_payment = true;
}
$gateway = CompanyGateway::find($request->input('company_gateway_id'));
/**
* find invoices
*
* ['invoice_id' => xxx, 'amount' => 22.00]
*/
$payable_invoices = collect($request->payable_invoices);
$invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
$invoices->each(function($invoice){
$invoice->service()->removeUnpaidGatewayFees()->save();
});
/* pop non payable invoice from the $payable_invoices array */
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) {
return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first()->isPayable();
});
/*return early if no invoices*/
if ($payable_invoices->count() == 0) {
return redirect()
->route('client.invoices.index')
->with(['message' => 'No payable invoices selected.']);
}
$settings = auth()->user()->client->getMergedSettings();
// nlog($settings);
/* This loop checks for under / over payments and returns the user if a check fails */
foreach ($payable_invoices as $payable_invoice) {
/*Match the payable invoice to the Model Invoice*/
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
/*
* Check if company supports over & under payments.
* Determine the payable amount and the max payable. ie either partial or invoice balance
*/
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
/*If we don't allow under/over payments force the payable amount - prevents inspect element adjustments in JS*/
if ($settings->client_portal_allow_under_payment == false && $settings->client_portal_allow_over_payment == false) {
$payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
}
if (!$settings->client_portal_allow_under_payment && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($settings->client_portal_allow_under_payment) {
if ($invoice_balance < $settings->client_portal_under_payment_minimum && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($invoice_balance < $settings->client_portal_under_payment_minimum) {
// Skip the under payment rule.
}
if ($invoice_balance >= $settings->client_portal_under_payment_minimum && $payable_amount < $settings->client_portal_under_payment_minimum) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]));
}
}
/* If we don't allow over payments and the amount exceeds the balance */
if (!$settings->client_portal_allow_over_payment && $payable_amount > $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.over_payments_disabled'));
}
}
/*Iterate through invoices and add gateway fees and other payment metadata*/
//$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
$payable_invoice_collection = collect();
foreach ($payable_invoices as $payable_invoice) {
// nlog($payable_invoice);
$payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']);
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), auth()->user()->client->currency()->precision);
$invoice_balance = Number::roundValue($invoice->balance, auth()->user()->client->currency()->precision);
$payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format());
$payable_invoice['invoice_number'] = $invoice->number;
if (isset($invoice->po_number)) {
$additional_info = $invoice->po_number;
} elseif (isset($invoice->public_notes)) {
$additional_info = $invoice->public_notes;
} else {
$additional_info = $invoice->date;
}
$payable_invoice['additional_info'] = $additional_info;
$payable_invoice_collection->push($payable_invoice);
}
//});
if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) {
$invoices->each(function ($invoice) use ($request) {
InjectSignature::dispatch($invoice, $request->signature);
});
}
$payable_invoices = $payable_invoice_collection;
$payment_method_id = $request->input('payment_method_id');
$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->balance;
if ($gateway) {
$first_invoice->service()->addGatewayFee($gateway, $payment_method_id, $invoice_totals)->save();
}
/**
* Gateway fee is calculated
* by adding it as a line item, and then subtract
* the starting and finishing amounts of the invoice.
*/
$fee_totals = $first_invoice->balance - $starting_invoice_amount;
if ($gateway) {
$tokens = auth()->user()->client->gateway_tokens()
->whereCompanyGatewayId($gateway->id)
->whereGatewayTypeId($payment_method_id)
->get();
}
if(!$is_credit_payment){
$credit_totals = 0;
}
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))];
if ($request->query('hash')) {
$hash_data['billing_context'] = Cache::get($request->query('hash'));
}
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(32);
$payment_hash->data = $hash_data;
$payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save();
if($is_credit_payment){
$amount_with_fee = max(0, (($invoice_totals + $fee_totals) - $credit_totals));
}
else{
$credit_totals = 0;
$amount_with_fee = max(0, $invoice_totals + $fee_totals);
}
$totals = [
'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals,
'amount_with_fee' => $amount_with_fee,
];
$data = [
'payment_hash' => $payment_hash->hash,
'total' => $totals,
'invoices' => $payable_invoices,
'tokens' => $tokens,
'payment_method_id' => $payment_method_id,
'amount_with_fee' => $invoice_totals + $fee_totals,
];
if ($is_credit_payment || $totals <= 0) {
return $this->processCreditPayment($request, $data);
}
try {
return $gateway
->driver(auth()->user()->client)
->setPaymentMethod($payment_method_id)
->setPaymentHash($payment_hash)
->checkRequirements()
->processPaymentView($data);
} catch (\Exception $e) {
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_FAILURE,
auth('contact')->user()->client,
auth('contact')->user()->client->company
);
throw new PaymentFailed($e->getMessage());
}
return (new InstantPayment($request))->run();
}
public function response(PaymentResponseRequest $request)

View File

@ -127,7 +127,7 @@ class EmailController extends BaseController
$entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
$entity_obj->service()->markSent()->save();

View File

@ -234,21 +234,24 @@ class MigrationController extends BaseController
public function startMigration(Request $request)
{
// v4 Laravel 6
// $companies = [];
// foreach($request->all() as $input){
// if($input instanceof UploadedFile)
// nlog('is file');
// else
// $companies[] = json_decode($input);
// }
nlog("Starting Migration");
$companies = json_decode($request->companies,1);
if($request->companies){
//handle Laravel 5.5 UniHTTP
$companies = json_decode($request->companies,1);
}
else {
//handle Laravel 6 Guzzle
$companies = [];
foreach($request->all() as $input){
if($input instanceof UploadedFile)
nlog('is file');
else
$companies[] = json_decode($input,1);
}
}
if (app()->environment() === 'local') {
nlog($request->all());
@ -267,22 +270,11 @@ class MigrationController extends BaseController
foreach($companies as $company)
{
if(!is_array($company))
continue;
$company = (array)$company;
// v4 Laravel 6
// $input = $request->all();
// foreach ($input as $company) {
// if($company instanceof UploadedFile)
// continue;
// else
// $company = json_decode($company,1);
// if (!$company || !is_int($company['company_index'] || !$request->file($company['company_index'])->isValid())) {
// continue;
// }
$user = auth()->user();
$company_count = $user->account->companies()->count();

View File

@ -41,8 +41,9 @@ class CreditsTable extends Component
->where('status_id', '<>', Credit::STATUS_DRAFT)
->where('is_deleted', 0)
->where(function ($query){
$query->whereDate('due_date', '<=', now())
->orWhereNull('due_date');
$query->whereDate('due_date', '>=', now())
->orWhereNull('due_date')
->orWhere('due_date', '=', '');
})
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()

View File

@ -37,7 +37,6 @@ class PaymentMethodsTable extends Component
->where('company_id', $this->company->id)
->where('client_id', $this->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.payment-methods-table', [

View File

@ -42,6 +42,9 @@ class SetInviteDb
$entity = $request->route('entity');
}
if($entity == "pay")
$entity = "invoice";
if ($request->getSchemeAndHttpHost() && config('ninja.db.multi_db_enabled') && ! MultiDB::findAndSetDbByInvitation($entity, $request->route('invitation_key'))) {
if (request()->json) {
return response()->json($error, 403);

View File

@ -25,15 +25,25 @@ class RegisterRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
$rules = [
'first_name' => ['required', 'string', 'max:255'],
'last_name' => ['required', 'string', 'max:255'],
'phone' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email:rfc,dns', 'max:255'],
'password' => ['required', 'string', 'min:6', 'confirmed'],
];
$rules = [];
foreach ($this->company()->client_registration_fields as $field) {
if ($field['required']) {
$rules[$field['key']] = ['required'];
}
}
foreach ($rules as $field => $properties) {
if ($field === 'email') {
$rules[$field] = array_merge($rules[$field], ['email:rfc,dns', 'max:255']);
}
if ($field === 'password') {
$rules[$field] = array_merge($rules[$field], ['string', 'min:6', 'confirmed']);
}
}
if ($this->company()->settings->client_portal_terms || $this->company()->settings->client_portal_privacy_policy) {
$rules['terms'] = ['required'];

View File

@ -51,6 +51,8 @@ class AutoBill
try{
nlog("autobill {$this->invoice->id}");
$this->invoice->service()->autoBill()->save();
}

View File

@ -25,6 +25,8 @@ class AutoBillCron
public $tries = 1;
private $counter = 1;
/**
* Create a new job instance.
*
@ -56,6 +58,7 @@ class AutoBillCron
->whereHas('company', function ($query) {
$query->where('is_disabled',0);
})
->orderBy('id', 'DESC')
->with('company');
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill");
@ -72,11 +75,11 @@ class AutoBillCron
->whereHas('company', function ($query) {
$query->where('is_disabled',0);
})
->orderBy('id', 'DESC')
->with('company');
nlog($auto_bill_invoices->count(). " full invoices to auto bill");
$auto_bill_invoices->cursor()->each(function ($invoice){
AutoBill::dispatch($invoice, false);
});
@ -96,6 +99,7 @@ class AutoBillCron
->whereHas('company', function ($query) {
$query->where('is_disabled',0);
})
->orderBy('id', 'DESC')
->with('company');
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill db = {$db}");
@ -112,15 +116,23 @@ class AutoBillCron
->whereHas('company', function ($query) {
$query->where('is_disabled',0);
})
->orderBy('id', 'DESC')
->with('company');
nlog($auto_bill_invoices->count(). " full invoices to auto bill db = {$db}");
$auto_bill_invoices->cursor()->each(function ($invoice) use($db){
nlog($this->counter);
AutoBill::dispatch($invoice, $db);
$this->counter++;
});
}
nlog("Auto Bill - fine");
}
}

View File

@ -59,15 +59,15 @@ class SendRecurring implements ShouldQueue
public function handle() : void
{
//reset all contacts here
$this->recurring_invoice->client->contacts()->update(['send_email' => false]);
// $this->recurring_invoice->client->contacts()->update(['send_email' => false]);
$this->recurring_invoice->invitations->each(function ($invitation){
// $this->recurring_invoice->invitations->each(function ($invitation){
$contact = $invitation->contact;
$contact->send_email = true;
$contact->save();
// $contact = $invitation->contact;
// $contact->send_email = true;
// $contact->save();
});
// });
// Generate Standard Invoice
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
@ -153,7 +153,7 @@ class SendRecurring implements ShouldQueue
}
//important catch all here - we should never leave contacts send_email to false incase they are permanently set to false in the future.
$this->recurring_invoice->client->contacts()->update(['send_email' => true]);
// $this->recurring_invoice->client->contacts()->update(['send_email' => true]);
}

View File

@ -93,6 +93,7 @@ class Gateway extends StaticModel
GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true],
GatewayType::KBC => ['refund' => false, 'token_billing' => false],
GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false],
GatewayType::IDEAL => ['refund' => false, 'token_billing' => false],
];
case 15:
return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal
@ -131,6 +132,10 @@ class Gateway extends StaticModel
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], //Square
];
break;
case 52:
return [
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true] // GoCardless
];
break;
default:
return [];

View File

@ -27,6 +27,7 @@ class GatewayType extends StaticModel
const CREDIT = 10;
const KBC = 11;
const BANCONTACT = 12;
const IDEAL = 13;
public function gateway()
{
@ -63,7 +64,8 @@ class GatewayType extends StaticModel
return ctrans('texts.kbc_cbc');
case self::BANCONTACT:
return ctrans('texts.bancontact');
case self::IDEAL:
return ctrans('texts.ideal');
default:
return 'Undefined.';
break;

View File

@ -45,6 +45,7 @@ class PaymentType extends StaticModel
const MOLLIE_BANK_TRANSFER = 34;
const KBC = 35;
const BANCONTACT = 36;
const IDEAL = 37;
public static function parseCardType($cardName)
{

View File

@ -73,6 +73,7 @@ class SystemLog extends Model
const TYPE_MOLLIE = 312;
const TYPE_EWAY = 313;
const TYPE_SQUARE = 320;
const TYPE_GOCARDLESS = 321;
const TYPE_QUOTA_EXCEEDED = 400;
const TYPE_UPSTREAM_FAILURE = 401;

View File

@ -395,7 +395,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->first()->invitations->each(function ($invitation) use ($nmo) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if ($invitation->contact->email) {
$nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo);
@ -459,7 +459,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->first()->invitations->each(function ($invitation) use ($nmo){
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
$nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo);
@ -492,81 +492,81 @@ class BaseDriver extends AbstractPaymentDriver
public function checkRequirements()
{
if ($this->company_gateway->require_billing_address) {
if ($this->checkRequiredResource(auth()->user('contact')->client->address1)) {
if ($this->checkRequiredResource($this->client->address1)) {
$this->required_fields[] = 'billing_address1';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->address2)) {
if ($this->checkRequiredResource($this->client->address2)) {
$this->required_fields[] = 'billing_address2';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->city)) {
if ($this->checkRequiredResource($this->client->city)) {
$this->required_fields[] = 'billing_city';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->state)) {
if ($this->checkRequiredResource($this->client->state)) {
$this->required_fields[] = 'billing_state';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->postal_code)) {
if ($this->checkRequiredResource($this->client->postal_code)) {
$this->required_fields[] = 'billing_postal_code';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->country_id)) {
if ($this->checkRequiredResource($this->client->country_id)) {
$this->required_fields[] = 'billing_country';
}
}
if ($this->company_gateway->require_shipping_address) {
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_address1)) {
if ($this->checkRequiredResource($this->client->shipping_address1)) {
$this->required_fields[] = 'shipping_address1';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_address2)) {
if ($this->checkRequiredResource($this->client->shipping_address2)) {
$this->required_fields[] = 'shipping_address2';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_city)) {
if ($this->checkRequiredResource($this->client->shipping_city)) {
$this->required_fields[] = 'shipping_city';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_state)) {
if ($this->checkRequiredResource($this->client->shipping_state)) {
$this->required_fields[] = 'shipping_state';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_postal_code)) {
if ($this->checkRequiredResource($this->client->shipping_postal_code)) {
$this->required_fields[] = 'shipping_postal_code';
}
if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_country_id)) {
if ($this->checkRequiredResource($this->client->shipping_country_id)) {
$this->required_fields[] = 'shipping_country';
}
}
if ($this->company_gateway->require_client_name) {
if ($this->checkRequiredResource(auth()->user('contact')->client->name)) {
if ($this->checkRequiredResource($this->client->name)) {
$this->required_fields[] = 'name';
}
}
if ($this->company_gateway->require_client_phone) {
if ($this->checkRequiredResource(auth()->user('contact')->client->phone)) {
if ($this->checkRequiredResource($this->client->phone)) {
$this->required_fields[] = 'phone';
}
}
if ($this->company_gateway->require_contact_email) {
if ($this->checkRequiredResource(auth()->user('contact')->email)) {
if ($this->checkRequiredResource($this->email)) {
$this->required_fields[] = 'contact_email';
}
}
if ($this->company_gateway->require_contact_name) {
if ($this->checkRequiredResource(auth()->user('contact')->first_name)) {
if ($this->checkRequiredResource($this->first_name)) {
$this->required_fields[] = 'contact_first_name';
}
if ($this->checkRequiredResource(auth()->user('contact')->last_name)) {
if ($this->checkRequiredResource($this->last_name)) {
$this->required_fields[] = 'contact_last_name';
}
}
@ -580,7 +580,7 @@ class BaseDriver extends AbstractPaymentDriver
}
}
if ($this->checkRequiredResource(auth()->user('contact')->client->postal_code)) {
if ($this->checkRequiredResource($this->client->postal_code)) {
$this->required_fields[] = 'postal_code';
}
}

View File

@ -0,0 +1,256 @@
<?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://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\GoCardless;
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\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\GoCardlessPaymentDriver;
use App\Utils\Traits\MakesHash;
use Exception;
use GoCardlessPro\Resources\Payment as ResourcesPayment;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\View\View;
class ACH implements MethodInterface
{
use MakesHash;
public GoCardlessPaymentDriver $go_cardless;
public function __construct(GoCardlessPaymentDriver $go_cardless)
{
$this->go_cardless = $go_cardless;
$this->go_cardless->init();
}
/**
* Authorization page for ACH.
*
* @param array $data
* @return Redirector|RedirectResponse
*/
public function authorizeView(array $data)
{
$session_token = \Illuminate\Support\Str::uuid()->toString();
try {
$redirect = $this->go_cardless->gateway->redirectFlows()->create([
"params" => [
"session_token" => $session_token,
"success_redirect_url" => route('client.payment_methods.confirm', [
'method' => GatewayType::BANK_TRANSFER,
'session_token' => $session_token,
]),
"prefilled_customer" => [
"given_name" => auth('contact')->user()->first_name,
"family_name" => auth('contact')->user()->last_name,
"email" => auth('contact')->user()->email,
"address_line1" => auth('contact')->user()->client->address1,
"city" => auth('contact')->user()->client->city,
"postal_code" => auth('contact')->user()->client->postal_code,
],
],
]);
return redirect(
$redirect->redirect_url
);
} catch (\Exception $exception) {
return $this->processUnsuccessfulAuthorization($exception);
}
}
/**
* Handle unsuccessful authorization.
*
* @param Exception $exception
* @throws PaymentFailed
* @return void
*/
public function processUnsuccessfulAuthorization(\Exception $exception): void
{
SystemLogger::dispatch(
$exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_GOCARDLESS,
$this->go_cardless->client,
$this->go_cardless->client->company,
);
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
/**
* Handle ACH post-redirect authorization.
*
* @param Request $request
* @return RedirectResponse|void
*/
public function authorizeResponse(Request $request)
{
try {
$redirect_flow = $this->go_cardless->gateway->redirectFlows()->complete(
$request->redirect_flow_id,
['params' => [
'session_token' => $request->session_token
]],
);
$payment_meta = new \stdClass;
$payment_meta->brand = ctrans('texts.ach');
$payment_meta->type = GatewayType::BANK_TRANSFER;
$payment_meta->state = 'authorized';
$data = [
'payment_meta' => $payment_meta,
'token' => $redirect_flow->links->mandate,
'payment_method_id' => GatewayType::BANK_TRANSFER,
];
$payment_method = $this->go_cardless->storeGatewayToken($data, ['gateway_customer_reference' => $redirect_flow->links->customer]);
return redirect()->route('client.payment_methods.show', $payment_method->hashed_id);
} catch (\Exception $exception) {
return $this->processUnsuccessfulAuthorization($exception);
}
}
/**
* Show the payment page for ACH.
*
* @param array $data
* @return View
*/
public function paymentView(array $data): View
{
$data['gateway'] = $this->go_cardless;
$data['amount'] = $this->go_cardless->convertToGoCardlessAmount($data['total']['amount_with_fee'], $this->go_cardless->client->currency()->precision);
$data['currency'] = $this->go_cardless->client->getCurrencyCode();
return render('gateways.gocardless.ach.pay', $data);
}
/**
* Process payments for ACH.
*
* @param PaymentResponseRequest $request
* @return RedirectResponse|void
*/
public function paymentResponse(PaymentResponseRequest $request)
{
$token = ClientGatewayToken::find(
$this->decodePrimaryKey($request->source)
)->firstOrFail();
try {
$payment = $this->go_cardless->gateway->payments()->create([
'params' => [
'amount' => $request->amount,
'currency' => $request->currency,
'metadata' => [
'payment_hash' => $this->go_cardless->payment_hash->hash,
],
'links' => [
'mandate' => $token->token,
],
],
]);
if ($payment->status === 'pending_submission') {
return $this->processPendingPayment($payment, ['token' => $token->hashed_id]);
}
return $this->processUnsuccessfulPayment($payment);
} catch (\Exception $exception) {
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
}
/**
* Handle pending payments for ACH.
*
* @param ResourcesPayment $payment
* @param array $data
* @return RedirectResponse
*/
public function processPendingPayment(\GoCardlessPro\Resources\Payment $payment, array $data = [])
{
$data = [
'payment_method' => $data['token'],
'payment_type' => PaymentType::ACH,
'amount' => $this->go_cardless->payment_hash->data->amount_with_fee,
'transaction_reference' => $payment->id,
'gateway_type_id' => GatewayType::BANK_TRANSFER,
];
$payment = $this->go_cardless->createPayment($data, Payment::STATUS_PENDING);
SystemLogger::dispatch(
['response' => $payment, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_GOCARDLESS,
$this->go_cardless->client,
$this->go_cardless->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->go_cardless->encodePrimaryKey($payment->id)]);
}
/**
* Process unsuccessful payments for ACH.
*
* @param ResourcesPayment $payment
* @return never
*/
public function processUnsuccessfulPayment(\GoCardlessPro\Resources\Payment $payment)
{
PaymentFailureMailer::dispatch($this->go_cardless->client, $payment->status, $this->go_cardless->client->company, $this->go_cardless->payment_hash->data->amount_with_fee);
PaymentFailureMailer::dispatch(
$this->go_cardless->client,
$payment,
$this->go_cardless->client->company,
$payment->amount
);
$message = [
'server_response' => $payment,
'data' => $this->go_cardless->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_GOCARDLESS,
$this->go_cardless->client,
$this->go_cardless->client->company,
);
throw new PaymentFailed('Failed to process the payment.', 500);
}
}

View File

@ -0,0 +1,253 @@
<?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://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\Utils\Traits\MakesHash;
class GoCardlessPaymentDriver extends BaseDriver
{
use MakesHash;
public $refundable = true;
public $token_billing = true;
public $can_authorise_credit_card = true;
public \GoCardlessPro\Client $gateway;
public $payment_method;
public static $methods = [
GatewayType::BANK_TRANSFER => \App\PaymentDrivers\GoCardless\ACH::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_GOCARDLESS;
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
public function gatewayTypes(): array
{
$types = [];
if (
$this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['USA'])
) {
$types[] = GatewayType::BANK_TRANSFER;
}
return $types;
}
public function init(): self
{
$this->gateway = new \GoCardlessPro\Client([
'access_token' => $this->company_gateway->getConfigField('accessToken'),
'environment' => $this->company_gateway->getConfigField('testMode') ? \GoCardlessPro\Environment::SANDBOX : \GoCardlessPro\Environment::LIVE,
]);
return $this;
}
public function authorizeView(array $data)
{
return $this->payment_method->authorizeView($data);
}
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request);
}
public function processPaymentView(array $data)
{
return $this->payment_method->paymentView($data);
}
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request);
}
public function refund(Payment $payment, $amount, $return_client_response = false)
{
// ..
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$converted_amount = $this->convertToGoCardlessAmount($amount, $this->client->currency()->precision);
$this->init();
try {
$payment = $this->gateway->payments()->create([
'params' => [
'amount' => $converted_amount,
'currency' => $this->client->getCurrencyCode(),
'metadata' => [
'payment_hash' => $this->payment_hash->hash,
],
'links' => [
'mandate' => $cgt->token,
],
],
]);
if ($payment->status === 'pending_submission') {
$this->confirmGatewayFee();
$data = [
'payment_method' => $cgt->hashed_id,
'payment_type' => PaymentType::ACH,
'amount' => $amount,
'transaction_reference' => $payment->id,
'gateway_type_id' => GatewayType::BANK_TRANSFER,
];
$payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $payment, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_GOCARDLESS,
$this->client,
$this->client->company
);
return $payment;
}
PaymentFailureMailer::dispatch(
$this->client,
$payment->status,
$this->client->company,
$amount
);
$message = [
'server_response' => $payment,
'data' => $payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_GOCARDLESS,
$this->client,
$this->client->company
);
return false;
} catch (\Exception $exception) {
$this->unWindGatewayFees($this->payment_hash);
$data = [
'status' => '',
'error_type' => '',
'error_code' => $exception->getCode(),
'param' => '',
'message' => $exception->getMessage(),
];
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_GOCARDLESS, $this->client, $this->client->company);
}
}
public function convertToGoCardlessAmount($amount, $precision)
{
return \round(($amount * pow(10, $precision)), 0);
}
public function detach(ClientGatewayToken $token)
{
$this->init();
try {
$this->gateway->mandates()->cancel($token->token);
} catch (\Exception $e) {
nlog($e->getMessage());
SystemLogger::dispatch(
[
'server_response' => $e->getMessage(),
'data' => request()->all(),
],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_GOCARDLESS,
$this->client,
$this->client->company
);
}
}
public function processWebhookRequest(PaymentWebhookRequest $request)
{
// Allow app to catch up with webhook request.
sleep(2);
$this->init();
foreach ($request->events as $event) {
if ($event['action'] === 'confirmed') {
$payment = Payment::query()
->where('transaction_reference', $event['links']['payment'])
->where('company_id', $request->getCompany()->id)
->first();
if ($payment) {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
}
}
if ($event['action'] === 'failed') {
// Update invoices, etc?
$payment = Payment::query()
->where('transaction_reference', $event['links']['payment'])
->where('company_id', $request->getCompany()->id)
->first();
if ($payment) {
$payment->status_id = Payment::STATUS_FAILED;
$payment->save();
}
}
}
return response()->json([], 200);
}
}

View File

@ -0,0 +1,215 @@
<?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://opensource.org/licenses/AAL
*/
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 Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class IDEAL implements MethodInterface
{
protected MolliePaymentDriver $mollie;
public function __construct(MolliePaymentDriver $mollie)
{
$this->mollie = $mollie;
$this->mollie->init();
}
/**
* Show the authorization page for iDEAL.
*
* @param array $data
* @return View
*/
public function authorizeView(array $data): View
{
return render('gateways.mollie.ideal.authorize', $data);
}
/**
* Handle the authorization for iDEAL.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
return redirect()->route('client.payment_methods.index');
}
/**
* Show the payment page for iDEAL.
*
* @param array $data
* @return Redirector|RedirectResponse
*/
public function paymentView(array $data)
{
$this->mollie->payment_hash
->withData('gateway_type_id', GatewayType::IDEAL)
->withData('client_id', $this->mollie->client->id);
try {
$payment = $this->mollie->gateway->payments->create([
'method' => 'ideal',
'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::IDEAL,
]),
'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 $exception
* @throws PaymentFailed
* @return void
*/
public function processUnsuccessfulPayment(\Exception $exception): void
{
PaymentFailureMailer::dispatch(
$this->mollie->client,
$exception->getMessage(),
$this->mollie->client->company,
$this->mollie->payment_hash->data->amount_with_fee
);
SystemLogger::dispatch(
$exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_MOLLIE,
$this->mollie->client,
$this->mollie->client->company,
);
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
/**
* Handle the payments for the iDEAL.
*
* @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);
}
if ($payment->status === 'failed') {
return $this->processUnsuccessfulPayment(
new PaymentFailed(ctrans('texts.status_failed'))
);
}
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 iDEAL.
*
* @param string $status
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment, string $status = 'paid'): RedirectResponse
{
$data = [
'gateway_type_id' => GatewayType::IDEAL,
'amount' => array_sum(array_column($this->mollie->payment_hash->invoices(), 'amount')) + $this->mollie->payment_hash->fee_total,
'payment_type' => PaymentType::IDEAL,
'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 IDEAL.
*
* @param ResourcesPayment $payment
* @return RedirectResponse
*/
public function processOpenPayment(\Mollie\Api\Resources\Payment $payment): RedirectResponse
{
return $this->processSuccessfulPayment($payment, 'open');
}
}

View File

@ -27,6 +27,7 @@ use App\Models\SystemLog;
use App\PaymentDrivers\Mollie\Bancontact;
use App\PaymentDrivers\Mollie\BankTransfer;
use App\PaymentDrivers\Mollie\CreditCard;
use App\PaymentDrivers\Mollie\IDEAL;
use App\PaymentDrivers\Mollie\KBC;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Validator;
@ -70,6 +71,7 @@ class MolliePaymentDriver extends BaseDriver
GatewayType::BANCONTACT => Bancontact::class,
GatewayType::BANK_TRANSFER => BankTransfer::class,
GatewayType::KBC => KBC::class,
GatewayType::IDEAL => IDEAL::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE;
@ -93,6 +95,7 @@ class MolliePaymentDriver extends BaseDriver
$types[] = GatewayType::BANCONTACT;
$types[] = GatewayType::BANK_TRANSFER;
$types[] = GatewayType::KBC;
$types[] = GatewayType::IDEAL;
return $types;
}

View File

@ -310,10 +310,6 @@ class BaseRepository
/* Perform model specific tasks */
if ($model instanceof Invoice) {
nlog("Finished amount = " . $state['finished_amount']);
nlog("Starting amount = " . $state['starting_amount']);
nlog("Diff = " . ($state['finished_amount'] - $state['starting_amount']));
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {
$model->service()->updateStatus()->save();

View File

@ -44,7 +44,7 @@ class SendEmail
}
$this->credit->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
$email_builder = (new CreditEmail())->build($invitation, $this->reminder_template);
// EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company);

View File

@ -23,6 +23,7 @@ use App\Models\PaymentType;
use App\Services\AbstractService;
use App\Utils\Ninja;
use Illuminate\Support\Str;
use PDO;
class AutoBillInvoice extends AbstractService
{
@ -113,10 +114,17 @@ class AutoBillInvoice extends AbstractService
nlog("Payment hash created => {$payment_hash->id}");
$payment = false;
try{
$payment = $gateway_token->gateway
->driver($this->client)
->setPaymentHash($payment_hash)
->tokenBilling($gateway_token, $payment_hash);
}
catch(\Exception $e){
nlog($e->getMessage());
}
if($payment){
info("Auto Bill payment captured for ".$this->invoice->number);
@ -296,7 +304,7 @@ class AutoBillInvoice extends AbstractService
{
//get all client gateway tokens and set the is_default one to the first record
$gateway_tokens = $this->client->gateway_tokens()->orderBy('is_default', 'DESC');
$gateway_tokens = $this->client->gateway_tokens()->orderBy('is_default', 'DESC')->get();
// $gateway_tokens = $this->client->gateway_tokens;
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use($amount) {
@ -304,7 +312,7 @@ class AutoBillInvoice extends AbstractService
$company_gateway = $gateway_token->gateway;
//check if fees and limits are set
if (isset($company_gateway->fees_and_limits) && property_exists($company_gateway->fees_and_limits, $gateway_token->gateway_type_id))
if (isset($company_gateway->fees_and_limits) && !is_array($company_gateway->fees_and_limits) && property_exists($company_gateway->fees_and_limits, $gateway_token->gateway_type_id))
{
//if valid we keep this gateway_token
if ($this->invoice->client->validGatewayForAmount($company_gateway->fees_and_limits->{$gateway_token->gateway_type_id}, $amount))

View File

@ -44,7 +44,7 @@ class SendEmail extends AbstractService
}
$this->invoice->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
}
});

View File

@ -33,7 +33,7 @@ class SendEmail
public function run()
{
$this->payment->client->contacts->each(function ($contact) {
if ($contact->send_email && $contact->email) {
if ($contact->email) {
EmailPayment::dispatchNow($this->payment, $this->payment->company, $contact);
}
});

View File

@ -77,8 +77,6 @@ class PdfMaker
$this->updateVariables($this->data['variables']);
}
$this->processOptions();
return $this;
}

View File

@ -163,144 +163,6 @@ trait PdfMakerUtilities
return $element;
}
public function processOptions()
{
if (!isset($this->options['all_pages_header']) || $this->options['all_pages_header'] == false) {
return;
}
if (!isset($this->options['all_pages_footer']) || $this->options['all_pages_footer'] == false) {
return;
}
$this->insertPrintCSS();
$this->wrapIntoTable();
}
public function insertPrintCSS()
{
$css = <<<'EOT'
table.page-container {
page-break-after: always;
min-width: 100%;
}
thead.page-header {
display: table-header-group;
}
tfoot.page-footer {
display: table-footer-group;
}
EOT;
$css_node = $this->document->createTextNode($css);
$style = $this->document->getElementsByTagName('style')->item(0);
if ($style) {
return $style->appendChild($css_node);
}
$head = $this->document->getElementsByTagName('head')->item(0);
if ($head) {
$style_node = $this->document->createElement('style', $css);
return $head->appendChild($style_node);
}
return $this;
}
public function wrapIntoTable()
{
$markup = <<<'EOT'
<table class="page-container" id="page-container">
<thead class="page-report">
<tr>
<th class="page-report-cell" id="repeat-header">
<!-- Repeating header goes here.. -->
</th>
</tr>
</thead>
<tfoot class="report-footer">
<tr>
<td class="report-footer-cell" id="repeat-footer">
<!-- Repeating footer goes here -->
</td>
</tr>
</tfoot>
<tbody class="report-content">
<tr>
<td class="report-content-cell" id="repeat-content">
<!-- Rest of the content goes here -->
</td>
</tr>
</tbody>
</table>
EOT;
$document = new DOMDocument();
$document->loadHTML($markup);
$table = $document->getElementById('page-container');
$body = $this->document->getElementsByTagName('body')
->item(0);
$body->appendChild(
$this->document->importNode($table, true)
);
for ($i = 0; $i < $body->childNodes->length; $i++) {
$element = $body->childNodes->item($i);
if ($element->nodeType !== 1) {
continue;
}
if (
$element->getAttribute('id') == 'header' ||
$element->getAttribute('id') == 'footer' ||
$element->getAttribute('id') === 'page-container'
) {
continue;
}
$clone = $element->cloneNode(true);
$element->parentNode->removeChild($element);
$this->document->getElementById('repeat-content')->appendChild($clone);
}
// info($this->data['options']);
if (
$header = $this->document->getElementById('header') &&
isset($this->data['options']['all_pages_header']) &&
$this->data['options']['all_pages_header']
) {
$header = $this->document->getElementById('header');
$clone = $header->cloneNode(true);
$header->parentNode->removeChild($header);
$this->document->getElementById('repeat-header')->appendChild($clone);
}
if (
$footer = $this->document->getElementById('footer') &&
isset($this->data['options']['all_pages_footer']) &&
$this->data['options']['all_pages_footer']
) {
$footer = $this->document->getElementById('footer');
$clone = $footer->cloneNode(true);
$footer->parentNode->removeChild($footer);
$this->document->getElementById('repeat-footer')->appendChild($clone);
}
}
public function getEmptyElements(array &$elements, array $variables)
{
foreach ($elements as &$element) {

View File

@ -39,7 +39,7 @@ class ConvertQuote
{
$invoice = CloneQuoteToInvoiceFactory::create($quote, $quote->user_id);
$invoice->design_id = $this->decodePrimaryKey($this->client->getSetting('invoice_design_id'));
$invoice = $this->invoice_repo->save([], $invoice);
$invoice = $this->invoice_repo->save($invoice->toArray(), $invoice);
$invoice->fresh();

View File

@ -42,7 +42,7 @@ class SendEmail
}
$this->quote->invitations->each(function ($invitation) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
}
});

View File

@ -129,6 +129,9 @@ class HtmlEngine
$data['$invoice.datetime'] = &$data['$entity.datetime'];
$data['$quote.datetime'] = &$data['$entity.datetime'];
$data['$credit.datetime'] = &$data['$entity.datetime'];
$data['$payment_button'] = ['value' => '<a class="button" href="'.$this->invitation->getPaymentLink().'">'.ctrans('texts.pay_now').'</a>', 'label' => ctrans('texts.pay_now')];
$data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')];
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')];
@ -140,7 +143,7 @@ class HtmlEngine
$data['$viewLink'] = &$data['$view_link'];
$data['$viewButton'] = &$data['$view_link'];
$data['$view_button'] = &$data['$view_link'];
$data['$paymentButton'] = &$data['$view_link'];
$data['$paymentButton'] = &$data['$payment_button'];
$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')];
@ -457,7 +460,7 @@ class HtmlEngine
$data['$auto_bill'] = &$data['$autoBill'];
/*Payment Aliases*/
$data['$paymentLink'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')];
$data['$paymentLink'] = &$data['$payment_link'];
$data['$portalButton'] = &$data['$paymentLink'];
$data['$dir'] = ['value' => optional($this->client->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];

View File

@ -82,11 +82,20 @@ class Phantom
$url = config('ninja.app_url').'/phantom/'.$entity.'/'.$invitation->key.'?phantomjs_secret='.config('ninja.phantomjs_secret');
info($url);
$key = config('ninja.phantomjs_key');
$secret = config('ninja.phantomjs_key');
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D";
$pdf = CurlUtils::get($phantom_url);
$key = config( 'ninja.phantomjs_key' );
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/";
$pdf = CurlUtils::post( $phantom_url, json_encode( [
'url' => $url,
'renderType' => 'pdf',
'outputAsJson' => false,
'renderSettings' => [
'emulateMedia' => 'print',
'pdfOptions' => [
'preferCSSPageSize' => true,
'printBackground' => true,
],
],
] ) );
$this->checkMime($pdf, $invitation, $entity);
@ -100,14 +109,20 @@ class Phantom
public function convertHtmlToPdf($html)
{
$hash = Str::random(32);
Cache::put($hash, $html, 300);
$url = route('tmp_pdf', ['hash' => $hash]);
info($url);
$key = config('ninja.phantomjs_key');
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D";
$pdf = CurlUtils::get($phantom_url);
$key = config( 'ninja.phantomjs_key' );
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/";
$pdf = CurlUtils::post( $phantom_url, json_encode( [
'content' => $html,
'renderType' => 'pdf',
'outputAsJson' => false,
'renderSettings' => [
'emulateMedia' => 'print',
'pdfOptions' => [
'preferCSSPageSize' => true,
'printBackground' => true,
],
],
] ) );
$response = Response::make($pdf, 200);
$response->header('Content-Type', 'application/pdf');

View File

@ -43,6 +43,17 @@ trait Inviteable
return $status;
}
public function getPaymentLink()
{
if(Ninja::isHosted()){
$domain = $this->company->domain();
}
else
$domain = config('ninja.app_url');
return $domain.'/client/pay/'.$this->key;
}
public function getLink() :string
{
$entity_type = Str::snake(class_basename($this->entityType()));

View File

@ -297,10 +297,6 @@ trait MakesInvoiceValues
$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]))
$data[$key][$table_type.'.gross_line_total'] = 0;
$data[$key][$table_type . ".{$_table_type}1"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}1", $item->custom_value1, $this->client);
$data[$key][$table_type . ".{$_table_type}2"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}2", $item->custom_value2, $this->client);
$data[$key][$table_type . ".{$_table_type}3"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}3", $item->custom_value3, $this->client);
@ -315,6 +311,10 @@ trait MakesInvoiceValues
$data[$key][$table_type.'.line_total'] = Number::formatMoney($item->line_total, $this->client);
if(property_exists($item, 'gross_line_total'))
$data[$key][$table_type.'.gross_line_total'] = Number::formatMoney($item->gross_line_total, $this->client);
else
$data[$key][$table_type.'.gross_line_total'] = 0;
if (isset($item->discount) && $item->discount > 0) {
if ($item->is_amount_discount) {

406
composer.lock generated

File diff suppressed because it is too large Load Diff

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.19',
'app_tag' => '5.3.19',
'app_version' => '5.3.21',
'app_tag' => '5.3.21',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,33 @@
<?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://opensource.org/licenses/AAL
*/
use App\Models\Gateway;
use Illuminate\Database\Migrations\Migration;
class ActivateGocardlessPaymentDriver extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$gateway = Gateway::find(52);
if ($gateway) {
$gateway->provider = 'GoCardless';
$gateway->visible = true;
$gateway->save();
}
}
}

View File

@ -0,0 +1,28 @@
<?php
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIdealToPaymentTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('payment_types', function (Blueprint $table) {
$type = new PaymentType();
$type->id = 37;
$type->name = 'iDEAL';
$type->gateway_type_id = GatewayType::IDEAL;
$type->save();
});
}
}

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdatedBoldAndModernDesigns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
\Illuminate\Support\Facades\Artisan::call('ninja:design-update');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -76,7 +76,7 @@ class PaymentLibrariesSeeder extends Seeder
['id' => 49, 'name' => 'WePay', 'provider' => 'WePay', 'is_offsite' => false, 'sort_order' => 3, 'key' => '8fdeed552015b3c7b44ed6c8ebd9e992', 'fields' => '{"accountId":"","accessToken":"","type":"goods","testMode":false,"feePayer":"payee"}'],
['id' => 50, 'name' => 'Braintree', 'provider' => 'Braintree', 'sort_order' => 3, 'key' => 'f7ec488676d310683fb51802d076d713', 'fields' => '{"merchantId":"","merchantAccountId":"","publicKey":"","privateKey":"","testMode":false}'],
['id' => 51, 'name' => 'FirstData Payeezy', 'provider' => 'FirstData_Payeezy', 'key' => '30334a52fb698046572c627ca10412e8', 'fields' => '{"gatewayId":"","password":"","keyId":"","hmac":"","testMode":false}'],
['id' => 52, 'name' => 'GoCardless', 'provider' => 'GoCardlessV2\Redirect', 'sort_order' => 9, 'is_offsite' => true, 'key' => 'b9886f9257f0c6ee7c302f1c74475f6c', 'fields' => '{"accessToken":"","webhookSecret":"","testMode":true}'],
['id' => 52, 'name' => 'GoCardless', 'provider' => 'GoCardless', 'sort_order' => 9, 'is_offsite' => true, 'key' => 'b9886f9257f0c6ee7c302f1c74475f6c', 'fields' => '{"accessToken":"","webhookSecret":"","testMode":true}'],
['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":""}'],
@ -97,7 +97,7 @@ class PaymentLibrariesSeeder extends Seeder
Gateway::query()->update(['visible' => 0]);
Gateway::whereIn('id', [1,7,11,15,20,39,46,55,50,57])->update(['visible' => 1]);
Gateway::whereIn('id', [1,7,11,15,20,39,46,55,50,57,52])->update(['visible' => 1]);
if (Ninja::isHosted()) {
Gateway::whereIn('id', [20])->update(['visible' => 0]);

View File

@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"/": "0d9690c2b925c794b94e0778817e5c19",
"/": "c89483e4d5b7e4169b05f1bc0cbe5935",
"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": "9542568225b6ad9e1ffbc87c3e6f74a2"
"main.dart.js": "45c1fed311a0bb61b3ec0e178d9bee9e"
};
// The application shell files that are downloaded before a service worker can

113168
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

107912
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

112966
public/main.last.dart.js vendored

File diff suppressed because one or more lines are too long

119642
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

110488
public/main.wasm.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -4316,6 +4316,7 @@ $LANG = array(
'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.',
'kbc_cbc' => 'KBC/CBC',
'bancontact' => 'Bancontact',
'ideal' => 'iDEAL',
);
return $LANG;

View File

@ -96,7 +96,7 @@
},
},
"content": {
"href": "https://www.invoiceninja.com/privacy-policy/",
"href": "{{ config('ninja.privacy_policy_url.hosted') }}",
"message": "This website uses cookies to ensure you get the best experience on our website.",
"dismiss": "Got it!",
"link": "Learn more",

View File

@ -13,31 +13,51 @@
zoom: 80%;
}
body, html {
margin: 0;
padding: 0;
}
@page {
margin: -0.25cm !important;
margin: 0 !important;
size: $page_size $page_layout;
}
p {
margin: 0;
padding: 0;
/* page-break-after: always; */
}
.header-wrapper {
#spacer-table > * > tr > td {
padding: 0;
}
#spacer-table{
width: 100%;
}
#header {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr;
gap: 20px;
background-color: #2d2c2a;
padding: 3rem;
color: white;
min-width: 100%;
line-height: var(--line-height);
position: fixed;
top: 0;
}
#header, #header-spacer {
height: 160px;
padding: 3rem;
margin-bottom: 3rem;
}
.company-logo {
height: 100%;
padding-right: 120px;
max-width: 100%;
object-fit: contain;
object-position: left center;
}
#company-details,
@ -46,6 +66,12 @@
flex-direction: column;
}
#company-details,
#company-address,
.logo-container {
max-height: 160px;
}
#client-details {
margin: 2rem;
display: flex;
@ -61,7 +87,6 @@
display: grid;
grid-template-columns: 1.5fr 1fr;
padding-left: 1rem;
padding-top: 3rem;
}
.entity-details-wrapper {
@ -86,7 +111,11 @@
table-layout: fixed;
overflow-wrap: break-word;
margin-top: 3rem;
/* margin-bottom: 200px; */
margin-bottom: 200px;
}
[data-ref="table"]:last-child{
margin-bottom:0;
}
.task-time-details {
@ -138,23 +167,23 @@
gap: 80px;
}
#table-totals .totals-table-right-side>* {
#table-totals .totals-table-right-side > * {
display: grid;
grid-template-columns: 1fr 1fr;
}
#table-totals>.totals-table-right-side>*> :nth-child(1) {
#table-totals > .totals-table-right-side > * > :nth-child(1) {
text-align: "$dir_text_align";
margin-top: .75rem;
}
#table-totals>.totals-table-right-side> * > :not([hidden]) ~ :not([hidden]) {
#table-totals > .totals-table-right-side > * > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(.75rem * var(--tw-space-y-reverse));
}
#table-totals>.totals-table-right-side>*> :nth-child(2) {
#table-totals > .totals-table-right-side > * > :nth-child(2) {
text-align: right;
}
@ -186,51 +215,40 @@
font-size: 1.5rem;
}
.footer-wrapper {
#footer {
margin-top: 1rem;
background-color: #2d2c2a;
height: 160px;
min-width: 100%;
position: fixed;
bottom: 0;
padding: 1rem 3rem;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 15px;
color: white;
}
#footer, #footer-spacer {
height: 160px;
padding: 1rem 3rem;
margin-top: 3rem;
}
[data-ref="total_table-footer"] {
padding-top: 0.5rem
}
/** Repeating header & footer styling. */
table {
width: 100%;
}
table.print-content {}
table.print-content th,
table.print-content td {
table[data-ref="table"] th,
table[data-ref="table"] td {
padding: .2rem .4rem;
text-align: left;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
@media print {
.print-footer {
position: fixed;
bottom: 0;
left: 0;
}
.no-print {
display: none
}
}
/** Markdown-specific styles. **/
#product-table h3,
#task-table h3,
@ -239,9 +257,9 @@
margin-bottom: 0;
}
[data-ref="product_table-product.description-th"] {
width: 23%;
}
[data-ref="product_table-product.description-th"] {
width: 23%;
}
[data-ref="statement-totals"] {
margin-top: 1rem;
@ -253,10 +271,6 @@
white-space: nowrap;
}
.logo-container {
max-height: 160px;
}
#statement-invoice-table-totals > p {
margin-right: 2rem;
margin-top: 1rem;
@ -264,101 +278,94 @@
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/
/* .company-logo { display: none } */
/** Hide company logo **/
/* .company-logo { display: none } */
/* Hide company details */
/* #company-details > * { display: none } */
/* Hide company details */
/* #company-details > * { display: none } */
/* Hide company address */
/* #company-address > * { display: none } */
/* Hide company address */
/* #company-address > * { display: none } */
/* Hide public notes */
/* [data-ref="total_table-public_notes"] { display: none } */
/* Hide public notes */
/* [data-ref="total_table-public_notes"] { display: none } */
/* Hide terms label */
/* [data-ref="total_table-terms-label"] { display: none } */
/* Hide terms label */
/* [data-ref="total_table-terms-label"] { display: none } */
/* Hide totals table */
/* #table-totals { display: none } */
/* Hide totals table */
/* #table-totals { display: none } */
/* Hide totals table left side */
/* #table-totals div:first-child > * { display: none !important } */
/* Hide totals table left side */
/* #table-totals div:first-child > * { display: none !important } */
/* Hide totals table right side */
/* .totals-table-right-side { display: none } */
/* Hide totals table right side */
/* .totals-table-right-side { display: none } */
/** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
/** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
</style>
<table>
<!-- Start Header -->
<thead>
<tr>
<td>
<div class="header-wrapper" id="header">
<div class="logo-container">
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
</div>
<div id="company-details"></div>
<div id="company-address"></div>
</div>
</td>
</tr>
</thead>
<!-- End Header -->
<tr>
<td id="body">
<div class="client-entity-wrapper">
<div class="client-wrapper-left-side">
<h4 class="entity-label">$entity_label</h4>
<div id="client-details" cellspacing="0"></div>
</div>
<div id="header">
<div class="logo-container">
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
</div>
<div id="company-details"></div>
<div id="company-address"></div>
</div>
<div id="body">
<table id="spacer-table" cellspacing="0" >
<thead>
<tr>
<td>
<div id="header-spacer"></div>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="client-entity-wrapper">
<div class="client-wrapper-left-side">
<h4 class="entity-label">$entity_label</h4>
<div id="client-details" cellspacing="0"></div>
</div>
<div class="entity-details-wrapper-right-side">
<div class="entity-details-wrapper">
<table id="entity-details" dir="$dir"></table>
<div class="entity-details-wrapper-right-side">
<div class="entity-details-wrapper">
<table id="entity-details" dir="$dir"></table>
</div>
</div>
</div>
</div>
<!-- Start Print Content -->
<table id="product-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="product-table" cellspacing="0" data-ref="table"></table>
<table id="task-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="task-table" cellspacing="0" data-ref="table"></table>
<table id="delivery-note-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
<table id="statement-invoice-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="statement-payment-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-payment-table" cellspacing="0" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-aging-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
<!-- End Print Content -->
</td>
</tr>
<tr>
<td>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
<!-- Start Space For Footer -->
<tfoot>
<tr>
<td style="height: 180px">
<!-- Leave this empty and don't remove it. This space is where footer placed on print. -->
</td>
</tr>
</tfoot>
<!-- End Space For Footer -->
</table>
<!-- Start Footer -->
<div class="footer-wrapper print-footer" id="footer">
<table id="statement-aging-table" cellspacing="0" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div id="footer-spacer"></div>
</td>
</tr>
</tfoot>
</table>
</div>
<div id="footer">
<div>
<p data-ref="total_table-footer">$entity_footer</p>
@ -366,14 +373,13 @@
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
document.addEventListener('DOMContentLoaded', () => {
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table-totals', 'statement-payment-table-totals','statement-invoice-table-totals',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table', 'statement-aging-table-totals', 'statement-payment-table-totals'
];
tables.forEach((tableIdentifier) => {
document.getElementById(tableIdentifier).childElementCount === 0
? document.getElementById(tableIdentifier).style.display = 'none'
: '';
const el =document.getElementById(tableIdentifier);
if(el && el.childElementCount === 0)el.remove()
});
});
</script>
@ -381,4 +387,3 @@
<div> <!-- #2 column --> </div>
<div> <!-- #3 column --> </div>
</div>
<!-- End Footer -->

View File

@ -9,38 +9,54 @@
-moz-osx-font-smoothing: grayscale;
font-family: Arial, Helvetica, sans-serif;
font-size: "7px";
margin: 0;
padding: 0;
zoom: 80%;
}
body, html {
margin: 0;
padding: 0;
}
@page {
margin: -0.22cm;
size: A4 portrait;
margin: 0 !important;
size: $page_size $page_layout;
}
p {
margin: 0;
padding: 0;
/* page-break-after: always; */
}
.header-container {
background-color: var(--primary-color);
color: white;
display: grid;
grid-template-columns: 1.5fr 1fr;
padding: 3rem;
min-width: 100%;
height: 160px;
#spacer-table > * > tr > td {
padding: 0;
}
#spacer-table{
width: 100%;
}
#header {
background-color: var(--primary-color);
color: white;
display: grid;
grid-template-columns: 1.5fr 1fr;
position:fixed;
top: 0;
width: 100%;
}
#header, #header-spacer {
height: 160px;
padding: 3rem;
margin-bottom: 3rem;
}
.company-name {
text-align: left;
}
.header-container .company-name {
font-size: 2rem;
#header .company-name {
font-size: 2rem;
}
#entity-details {
@ -55,7 +71,7 @@
}
.logo-client-wrapper {
margin: 3rem 2rem;
margin: 0 2rem 3rem;
display: grid;
grid-template-columns: 1.5fr 1fr;
}
@ -121,15 +137,18 @@
text-align: right;
}
.footer-wrapper {
margin-top: 1rem;
background-color: var(--primary-color);
padding-left: 3rem;
padding-right: 3rem;
height: 220px;
width: 100%;
position: fixed;
bottom: 0;
#footer {
background-color: var(--primary-color);
width: 100%;
position: fixed;
bottom: 0;
}
#footer, #footer-spacer {
height: 220px;
padding: 1rem 3rem;
margin-top: 1rem;
}
.footer-content {
@ -204,54 +223,21 @@
font-size: 1.3rem;
}
table.page-container {
page-break-after: always;
min-width: 100%;
}
thead.page-header {
display: table-header-group;
}
tfoot.page-footer {
display: table-footer-group;
}
.page-content-cell {
padding: 1rem;
}
[data-ref="total_table-footer"] {
margin-top: 2rem;
margin-bottom: 2rem;
margin-top: 2rem;
margin-bottom: 2rem;
}
/** Repeating header & footer styling. */
table {
width: 100%;
width: 100%;
}
table.print-content {
}
table.print-content th,
table.print-content td {
padding: 0.2rem 0.4rem;
text-align: left;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
@media print {
.print-footer {
position: fixed;
bottom: 0;
left: 0;
}
.no-print {
display: none;
}
table[data-ref="table"] th,
table[data-ref="table"] td {
padding: 0.2rem 0.4rem;
text-align: left;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
/** Markdown-specific styles. **/
@ -306,70 +292,57 @@
/** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
</style>
<div id="header">
<h1 class="company-name">$company.name</h1>
<table id="entity-details" cellspacing="0" dir="$dir"></table>
</div>
<div id="body">
<table id="spacer-table" cellspacing="0">
<thead>
<tr>
<td>
<div id="header-spacer"></div>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="logo-client-wrapper">
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
<div id="client-details"></div>
</div>
<div class="table-wrapper">
<table id="product-table" cellspacing="0" data-ref="table"></table>
<table>
<!-- Start Header -->
<thead>
<tr>
<td>
<div class="header-container" id="header">
<h1 class="company-name">$company.name</h1>
<table id="entity-details" cellspacing="0" dir="$dir"></table>
</div>
</td>
</tr>
</thead>
<!-- End Header -->
<tr>
<td id="body">
<div class="logo-client-wrapper">
<img
class="company-logo"
src="$company.logo"
alt="$company.name logo"
/>
<table id="task-table" cellspacing="0" data-ref="table"></table>
<div id="client-details"></div>
</div>
<table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
<!-- Start Print Content -->
<div class="table-wrapper">
<table id="product-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="task-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="statement-payment-table" cellspacing="0" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="delivery-note-table" cellspacing="0" class="print-content" data-ref="table"></table>
<table id="statement-aging-table" cellspacing="0" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
</div>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div id="footer-spacer"></div>
</td>
</tr>
</tfoot>
</table>
</div>
<table id="statement-invoice-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
<table id="statement-payment-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-aging-table" cellspacing="0" class="print-content" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
</div>
<!-- End Print Content -->
</td>
</tr>
<tr>
<td>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
<!-- Start Space For Footer -->
<tfoot>
<tr>
<td style="height: 230px">
<!-- Leave this empty and don't remove it. This space is where footer placed on print -->
</td>
</tr>
</tfoot>
<!-- End Space For Footer -->
</table>
<!-- Start Footer -->
<div class="footer-wrapper print-footer" id="footer">
<div id="footer">
<div class="footer-content">
<div>
<p data-ref="total_table-footer">$entity_footer</p>
@ -378,14 +351,14 @@
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
document.addEventListener('DOMContentLoaded', () => {
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'product-table', 'task-table', 'delivery-note-table', 'statement-invoice-table-totals', 'statement-payment-table-totals','statement-invoice-table-totals',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table', 'statement-aging-table-totals', 'statement-payment-table-totals'
];
tables.forEach((tableIdentifier) => {
document.getElementById(tableIdentifier).childElementCount === 0
? document.getElementById(tableIdentifier).style.display = 'none'
: '';
? document.getElementById(tableIdentifier).remove()
: '';
});
});
</script>
@ -396,4 +369,3 @@
</div>
</div>
</div>
<!-- End Footer -->

View File

@ -1,74 +0,0 @@
<!-- Client personal address -->
<h3 class="text-lg font-medium leading-6 text-gray-900 mt-8">{{ ctrans('texts.personal_address') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ ctrans('texts.enter_your_personal_address') }}
</p>
<div class="shadow overflow-hidden rounded mt-4">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-4">
<label for="address1" class="input-label">{{ ctrans('texts.address1') }}</label>
<input id="address1" class="input w-full" name="address1"/>
@error('address1')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="address2" class="input-label">{{ ctrans('texts.address2') }}</label>
<input id="address2" class="input w-full" name="address2"/>
@error('address2')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="city" class="input-label">{{ ctrans('texts.city') }}</label>
<input id="city" class="input w-full" name="city"/>
@error('city')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="state" class="input-label">{{ ctrans('texts.state') }}</label>
<input id="state" class="input w-full" name="state"/>
@error('state')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="postal_code" class="input-label">{{ ctrans('texts.postal_code') }}</label>
<input id="postal_code" class="input w-full" name="postal_code"/>
@error('postal_code')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="country" class="input-label">{{ ctrans('texts.country') }}</label>
<select id="country" class="input w-full form-select" name="country">
<option value="none"></option>
@foreach(App\Utils\TranslationHelper::getCountries() as $country)
<option value="{{ $country->id }}">
{{ $country->iso_3166_2 }} ({{ $country->name }})
</option>
@endforeach
</select>
@error('country')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>

View File

@ -1,90 +0,0 @@
<!-- Personal info, first name, last name, e-mail address .. -->
<h3 class="text-lg font-medium leading-6 text-gray-900 mt-8">{{ ctrans('texts.profile') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ ctrans('texts.client_information_text') }}
</p>
<div class="shadow overflow-hidden rounded mt-4">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<section class="flex items-center">
<label for="first_name" class="input-label">{{ ctrans('texts.first_name') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="first_name" class="input w-full" name="first_name" />
@error('first_name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<section class="flex items-center">
<label for="last_name" class="input-label">{{ ctrans('texts.last_name') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="last_name" class="input w-full" name="last_name" />
@error('last_name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-4">
<section class="flex items-center">
<label for="email_address" class="input-label">{{ ctrans('texts.email_address') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="email_address" class="input w-full" type="email" name="email" />
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-4">
<section class="flex items-center">
<label for="phone" class="input-label">{{ ctrans('texts.phone') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="phone" class="input w-full" name="phone" />
@error('phone')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-6 lg:col-span-3">
<section class="flex items-center">
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="password" class="input w-full" name="password" type="password" />
@error('password')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3 lg:col-span-3">
<section class="flex items-center">
<label for="password_confirmation" class="input-label">{{ ctrans('texts.confirm_password') }}</label>
<section class="text-red-400 ml-1 text-sm">*</section>
</section>
<input id="state" class="input w-full" name="password_confirmation" type="password" />
@error('password_confirmation')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>

View File

@ -1,88 +0,0 @@
<!-- Client shipping address -->
<h3 class="text-lg font-medium leading-6 text-gray-900 mt-8">{{ ctrans('texts.shipping_address') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ ctrans('texts.enter_your_shipping_address') }}
</p>
<div class="shadow overflow-hidden rounded mt-4">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-4">
<label for="shipping_address1" class="input-label">{{ ctrans('texts.shipping_address1') }}</label>
<input id="shipping_address1"
class="input w-full {{ in_array('shipping_address1', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
name="shipping_address1"/>
@error('shipping_address1')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="shipping_address2" class="input-label">{{ ctrans('texts.shipping_address2') }}</label>
<input
id="shipping_address2 {{ in_array('shipping_address2', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
class="input w-full" name="shipping_address2"/>
@error('shipping_address2')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="shipping_city" class="input-label">{{ ctrans('texts.shipping_city') }}</label>
<input id="shipping_city"
class="input w-full {{ in_array('shipping_city', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
name="shipping_city"/>
@error('shipping_city')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="shipping_state" class="input-label">{{ ctrans('texts.shipping_state') }}</label>
<input id="shipping_state"
class="input w-ful {{ in_array('shipping_state', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}l"
name="shipping_state"/>
@error('shipping_state')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="shipping_postal_code" class="input-label">{{ ctrans('texts.shipping_postal_code') }}</label>
<input id="shipping_postal_code"
class="input w-full {{ in_array('shipping_postal_code', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
name="shipping_postal_code"/>
@error('shipping_postal_code')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-4 sm:col-span-2">
<label for="shipping_country" class="input-label">{{ ctrans('texts.shipping_country') }}</label>
<select id="shipping_country"
class="input w-full form-select {{ in_array('shipping_country', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
name="shipping_country">
@foreach(App\Utils\TranslationHelper::getCountries() as $country)
<option value="none"></option>
<option
{{ $country == isset(auth()->user()->client->shipping_country->id) ? 'selected' : null }} value="{{ $country->id }}">
{{ $country->iso_3166_2 }}
({{ $country->name }})
</option>
@endforeach
</select>
@error('country')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>

View File

@ -1,32 +0,0 @@
<!-- Name, website -->
<h3 class="text-lg font-medium leading-6 text-gray-900 mt-8">{{ ctrans('texts.website') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ ctrans('texts.make_sure_use_full_link') }}
</p>
<div class="shadow overflow-hidden rounded mt-4">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="street" class="input-label">{{ ctrans('texts.name') }}</label>
<input id="name" class="input w-full" name="name" />
@error('name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="website" class="input-label">{{ ctrans('texts.website') }}</label>
<input id="website" class="input w-full" name="website" />
@error('website')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>

View File

@ -12,14 +12,90 @@
<form action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{ more: false }">
@csrf
@include('portal.ninja2020.auth.includes.register.personal_information')
<span class="block mt-4 text-gray-800 hover:text-gray-900 text-right cursor-pointer" x-on:click="more = !more">{{ ctrans('texts.more_fields') }}</span>
<div class="grid grid-cols-12 gap-4 mt-10">
@foreach($company->client_registration_fields as $field)
@if($field['required'])
<div class="col-span-12 md:col-span-6">
<section class="flex items-center">
<label
for="{{ $field['key'] }}"
class="input-label">
{{ ctrans("texts.{$field['key']}") }}
</label>
<div x-show="more">
@include('portal.ninja2020.auth.includes.register.website')
@include('portal.ninja2020.auth.includes.register.personal_address')
@include('portal.ninja2020.auth.includes.register.shipping_address')
@if($field['required'])
<section class="text-red-400 ml-1 text-sm">*</section>
@endif
</section>
@if($field['key'] === 'email')
<input
id="{{ $field['key'] }}"
class="input w-full"
type="email"
name="{{ $field['key'] }}"
{{ $field['required'] ? 'required' : '' }} />
@elseif($field['key'] === 'password')
<input
id="{{ $field['key'] }}"
class="input w-full"
type="password"
name="{{ $field['key'] }}"
{{ $field['required'] ? 'required' : '' }} />
@elseif($field['key'] === 'country_id')
<select
id="shipping_country"
class="input w-full form-select"
name="shipping_country">
<option value="none"></option>
@foreach(App\Utils\TranslationHelper::getCountries() as $country)
<option
{{ $country == isset(auth()->user()->client->shipping_country->id) ? 'selected' : null }} value="{{ $country->id }}">
{{ $country->iso_3166_2 }}
({{ $country->name }})
</option>
@endforeach
</select>
@else
<input
id="{{ $field['key'] }}"
class="input w-full"
name="{{ $field['key'] }}"
{{ $field['required'] ? 'required' : '' }} />
@endif
@error($field['key'])
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
@if($field['key'] === 'password')
<div class="col-span-12 md:col-span-6">
<section class="flex items-center">
<label
for="password_confirmation"
class="input-label">
{{ ctrans('texts.password_confirmation') }}
</label>
@if($field['required'])
<section class="text-red-400 ml-1 text-sm">*</section>
@endif
</section>
<input
id="password_confirmation"
type="password"
class="input w-full"
name="password_confirmation"
{{ $field['required'] ? 'required' : '' }} />
</div>
@endif
@endif
@endforeach
</div>
<div class="flex justify-between items-center mt-8">

View File

@ -0,0 +1,55 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH'])
@section('gateway_content')
@if (count($tokens) > 0)
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@include('portal.ninja2020.gateways.includes.payment_details')
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="source" value="">
<input type="hidden" name="amount" value="{{ $amount }}">
<input type="hidden" name="currency" value="{{ $currency }}">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
</form>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
@if (count($tokens) > 0)
@foreach ($tokens as $token)
<label class="mr-4">
<input type="radio" data-token="{{ $token->hashed_id }}" name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token" />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.bank_transfer') }}
(#{{ $token->hashed_id }})</span>
</label>
@endforeach
@endisset
@endcomponent
@else
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
<span>{{ ctrans('texts.bank_account_not_linked') }}</span>
<a class="button button-link text-primary"
href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
@endcomponent
@endif
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@push('footer')
<script>
Array
.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => element.addEventListener('click', (element) => {
document.querySelector('input[name=source]').value = element.target.dataset.token;
}));
document.getElementById('pay-now').addEventListener('click', function() {
document.getElementById('server-response').submit();
});
</script>
@endpush

View File

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

View File

@ -114,7 +114,7 @@
},
},
"content": {
"href": "https://www.invoiceninja.com/privacy-policy/",
"href": "{{ config('ninja.privacy_policy_url.hosted') }}",
"message": "This website uses cookies to ensure you get the best experience on our website.",
"dismiss": "Got it!",
"link": "Learn more",

View File

@ -97,7 +97,7 @@
},
},
"content": {
"href": "https://www.invoiceninja.com/privacy-policy/",
"href": "{{ config('ninja.privacy_policy_url.hosted') }}",
"message": "This website uses cookies to ensure you get the best experience on our website.",
"dismiss": "Got it!",
"link": "Learn more",

View File

@ -107,7 +107,7 @@
},
},
"content": {
"href": "https://www.invoiceninja.com/privacy-policy/",
"href": "{{ config('ninja.privacy_policy_url.hosted') }}",
"message": "This website uses cookies to ensure you get the best experience on our website.",
"dismiss": "Got it!",
"link": "Learn more",

View File

@ -39,7 +39,7 @@
<input type="checkbox" class="form-checkbox mr-1"
name="send_logs" {{ old('send_logs' ? 'checked': '') }}>
<span>{{ ctrans('texts.send_fail_logs_to_our_server') }}</span>
<a class="button-link mt-1 block" href="https://www.invoiceninja.com/privacy-policy/">Read more
<a class="button-link mt-1 block" target="_blank" href="https://www.invoiceninja.com/privacy-policy/">Read more
about how we use this.</a>
</dd>
</div>

View File

@ -10,7 +10,12 @@
<div class="col-span-12 md:col-start-4 md:col-span-6 mt-4 md:mt-10">
<h1 class="text-center text-2xl font-semibold">Invoice Ninja Setup</h1>
<p class="text-sm text-center">{{ ctrans('texts.if_you_need_help') }}
<a href="https://www.invoiceninja.com/forums/forum/support/" class="button-link">{{ ctrans('texts.support_forum') }}</a>
<a
target="_blank"
href="https://www.invoiceninja.com/forums/forum/support/"
class="button-link underline">
{{ ctrans('texts.support_forum') }}
</a>
</p>
@if($errors->any())

View File

@ -95,7 +95,7 @@
},
},
"content": {
"href": "https://www.invoiceninja.com/privacy-policy/",
"href": "{{ config('ninja.privacy_policy_url.hosted') }}",
"message": "This website uses cookies to ensure you get the best experience on our website.",
"dismiss": "Got it!",
"link": "Learn more",

View File

@ -56,6 +56,8 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
Route::get('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@verify')->name('payment_methods.verification');
Route::post('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@processVerification');
Route::get('payment_methods/confirm', 'ClientPortal\PaymentMethodController@store')->name('payment_methods.confirm');
Route::resource('payment_methods', 'ClientPortal\PaymentMethodController')->except(['edit', 'update']);
Route::match(['GET', 'POST'], 'quotes/approve', 'ClientPortal\QuoteController@bulk')->name('quotes.bulk');
@ -101,6 +103,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf')->name('quote.download_invitation_key');
Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key');
Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
Route::get('pay/{invitation_key}', 'ClientPortal\InvitationController@payInvoice')->name('pay.invoice');
// Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe')->name('invoice.client_hash_and_invitation_key'); //should never need this
});

View File

@ -0,0 +1,42 @@
<?php
namespace Tests\Browser\ClientPortal\Gateways\GoCardless;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class ACHTest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', 'b9886f9257f0c6ee7c302f1c74475f6c')->restore();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPayingWithNoPreauthorizedIsntPossible()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('Bank Transfer')
->assertSee('To pay with a bank account, first you have to add it as payment method.');
});
}
}

View File

@ -0,0 +1,106 @@
<?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 IDEALTest 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('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'paid')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Completed');
});
}
public function testOpenPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'open')
->press('Continue')
->waitForText('Details of the payment')
->assertSee('Pending');
});
}
public function testFailedPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'failed')
->press('Continue')
->waitForText('Failed.');
});
}
public function testCancelledPayment(): void
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('iDEAL')
->waitForText('Test profile')
->press('ABN AMRO')
->radio('final_state', 'canceled')
->press('Continue')
->waitForText('Cancelled.');
});
}
}

View File

@ -93,9 +93,9 @@ class CreditsTest extends TestCase
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company])
->assertSee('testing-number-01')
->assertDontSee('testing-number-01')
->assertSee('testing-number-02')
->assertDontSee('testing-number-03');
->assertSee('testing-number-03');
}
public function testShowingCreditsWithNullDueDate()
@ -122,6 +122,7 @@ class CreditsTest extends TestCase
'client_id' => $client->id,
'number' => 'testing-number-01',
'status_id' => Credit::STATUS_SENT,
'due_date' => null,
]);
Credit::factory()->create([
@ -142,12 +143,21 @@ class CreditsTest extends TestCase
'status_id' => Credit::STATUS_SENT,
]);
Credit::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'number' => 'testing-number-04',
'due_date' => '',
'status_id' => Credit::STATUS_SENT,
]);
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company])
->assertSee('testing-number-01')
->assertSee('testing-number-02')
->assertDontSee('testing-number-03');
->assertSee('testing-number-03');
}
}