mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
commit
c3ab4aa0b2
@ -1 +1 @@
|
|||||||
5.3.19
|
5.3.21
|
@ -28,9 +28,9 @@ class CloneQuoteToInvoiceFactory
|
|||||||
unset($quote_array['invoice_id']);
|
unset($quote_array['invoice_id']);
|
||||||
unset($quote_array['id']);
|
unset($quote_array['id']);
|
||||||
unset($quote_array['invitations']);
|
unset($quote_array['invitations']);
|
||||||
unset($quote_array['terms']);
|
//unset($quote_array['terms']);
|
||||||
// unset($quote_array['public_notes']);
|
//unset($quote_array['public_notes']);
|
||||||
unset($quote_array['footer']);
|
//unset($quote_array['footer']);
|
||||||
unset($quote_array['design_id']);
|
unset($quote_array['design_id']);
|
||||||
|
|
||||||
foreach ($quote_array as $key => $value) {
|
foreach ($quote_array as $key => $value) {
|
||||||
|
@ -19,7 +19,10 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Jobs\Entity\CreateRawPdf;
|
use App\Jobs\Entity\CreateRawPdf;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\ClientContact;
|
use App\Models\ClientContact;
|
||||||
|
use App\Models\InvoiceInvitation;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
|
use App\Services\ClientPortal\InstantPayment;
|
||||||
|
use App\Utils\CurlUtils;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
use App\Utils\Traits\MakesDates;
|
use App\Utils\Traits\MakesDates;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
@ -65,6 +68,9 @@ class InvitationController extends Controller
|
|||||||
private function genericRouter(string $entity, string $invitation_key)
|
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';
|
$key = $entity.'_id';
|
||||||
|
|
||||||
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
|
$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)
|
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';
|
$key = $entity.'_id';
|
||||||
|
|
||||||
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
|
$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);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ use App\Models\Invoice;
|
|||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Models\PaymentHash;
|
use App\Models\PaymentHash;
|
||||||
use App\Models\SystemLog;
|
use App\Models\SystemLog;
|
||||||
|
use App\Services\ClientPortal\InstantPayment;
|
||||||
use App\Services\Subscription\SubscriptionService;
|
use App\Services\Subscription\SubscriptionService;
|
||||||
use App\Utils\Number;
|
use App\Utils\Number;
|
||||||
use App\Utils\Traits\MakesDates;
|
use App\Utils\Traits\MakesDates;
|
||||||
@ -79,235 +80,7 @@ class PaymentController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function process(Request $request)
|
public function process(Request $request)
|
||||||
{
|
{
|
||||||
$is_credit_payment = false;
|
return (new InstantPayment($request))->run();
|
||||||
$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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function response(PaymentResponseRequest $request)
|
public function response(PaymentResponseRequest $request)
|
||||||
|
@ -127,7 +127,7 @@ class EmailController extends BaseController
|
|||||||
|
|
||||||
$entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) {
|
$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();
|
$entity_obj->service()->markSent()->save();
|
||||||
|
|
||||||
|
@ -234,21 +234,24 @@ class MigrationController extends BaseController
|
|||||||
public function startMigration(Request $request)
|
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");
|
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') {
|
if (app()->environment() === 'local') {
|
||||||
nlog($request->all());
|
nlog($request->all());
|
||||||
@ -267,22 +270,11 @@ class MigrationController extends BaseController
|
|||||||
foreach($companies as $company)
|
foreach($companies as $company)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
if(!is_array($company))
|
||||||
|
continue;
|
||||||
|
|
||||||
$company = (array)$company;
|
$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();
|
$user = auth()->user();
|
||||||
|
|
||||||
$company_count = $user->account->companies()->count();
|
$company_count = $user->account->companies()->count();
|
||||||
|
@ -41,8 +41,9 @@ class CreditsTable extends Component
|
|||||||
->where('status_id', '<>', Credit::STATUS_DRAFT)
|
->where('status_id', '<>', Credit::STATUS_DRAFT)
|
||||||
->where('is_deleted', 0)
|
->where('is_deleted', 0)
|
||||||
->where(function ($query){
|
->where(function ($query){
|
||||||
$query->whereDate('due_date', '<=', now())
|
$query->whereDate('due_date', '>=', now())
|
||||||
->orWhereNull('due_date');
|
->orWhereNull('due_date')
|
||||||
|
->orWhere('due_date', '=', '');
|
||||||
})
|
})
|
||||||
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
|
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
|
||||||
->withTrashed()
|
->withTrashed()
|
||||||
|
@ -37,7 +37,6 @@ class PaymentMethodsTable extends Component
|
|||||||
->where('company_id', $this->company->id)
|
->where('company_id', $this->company->id)
|
||||||
->where('client_id', $this->client->id)
|
->where('client_id', $this->client->id)
|
||||||
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
|
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
|
||||||
->withTrashed()
|
|
||||||
->paginate($this->per_page);
|
->paginate($this->per_page);
|
||||||
|
|
||||||
return render('components.livewire.payment-methods-table', [
|
return render('components.livewire.payment-methods-table', [
|
||||||
|
@ -42,6 +42,9 @@ class SetInviteDb
|
|||||||
$entity = $request->route('entity');
|
$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->getSchemeAndHttpHost() && config('ninja.db.multi_db_enabled') && ! MultiDB::findAndSetDbByInvitation($entity, $request->route('invitation_key'))) {
|
||||||
if (request()->json) {
|
if (request()->json) {
|
||||||
return response()->json($error, 403);
|
return response()->json($error, 403);
|
||||||
|
@ -25,15 +25,25 @@ class RegisterRequest extends FormRequest
|
|||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function rules()
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [];
|
||||||
'first_name' => ['required', 'string', 'max:255'],
|
|
||||||
'last_name' => ['required', 'string', 'max:255'],
|
foreach ($this->company()->client_registration_fields as $field) {
|
||||||
'phone' => ['required', 'string', 'max:255'],
|
if ($field['required']) {
|
||||||
'email' => ['required', 'string', 'email:rfc,dns', 'max:255'],
|
$rules[$field['key']] = ['required'];
|
||||||
'password' => ['required', 'string', 'min:6', 'confirmed'],
|
}
|
||||||
];
|
}
|
||||||
|
|
||||||
|
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) {
|
if ($this->company()->settings->client_portal_terms || $this->company()->settings->client_portal_privacy_policy) {
|
||||||
$rules['terms'] = ['required'];
|
$rules['terms'] = ['required'];
|
||||||
|
@ -50,6 +50,8 @@ class AutoBill
|
|||||||
MultiDB::setDb($this->db);
|
MultiDB::setDb($this->db);
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
|
||||||
|
nlog("autobill {$this->invoice->id}");
|
||||||
|
|
||||||
$this->invoice->service()->autoBill()->save();
|
$this->invoice->service()->autoBill()->save();
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@ class AutoBillCron
|
|||||||
|
|
||||||
public $tries = 1;
|
public $tries = 1;
|
||||||
|
|
||||||
|
private $counter = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new job instance.
|
* Create a new job instance.
|
||||||
*
|
*
|
||||||
@ -56,6 +58,7 @@ class AutoBillCron
|
|||||||
->whereHas('company', function ($query) {
|
->whereHas('company', function ($query) {
|
||||||
$query->where('is_disabled',0);
|
$query->where('is_disabled',0);
|
||||||
})
|
})
|
||||||
|
->orderBy('id', 'DESC')
|
||||||
->with('company');
|
->with('company');
|
||||||
|
|
||||||
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill");
|
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill");
|
||||||
@ -72,11 +75,11 @@ class AutoBillCron
|
|||||||
->whereHas('company', function ($query) {
|
->whereHas('company', function ($query) {
|
||||||
$query->where('is_disabled',0);
|
$query->where('is_disabled',0);
|
||||||
})
|
})
|
||||||
|
->orderBy('id', 'DESC')
|
||||||
->with('company');
|
->with('company');
|
||||||
|
|
||||||
nlog($auto_bill_invoices->count(). " full invoices to auto bill");
|
nlog($auto_bill_invoices->count(). " full invoices to auto bill");
|
||||||
|
|
||||||
|
|
||||||
$auto_bill_invoices->cursor()->each(function ($invoice){
|
$auto_bill_invoices->cursor()->each(function ($invoice){
|
||||||
AutoBill::dispatch($invoice, false);
|
AutoBill::dispatch($invoice, false);
|
||||||
});
|
});
|
||||||
@ -96,6 +99,7 @@ class AutoBillCron
|
|||||||
->whereHas('company', function ($query) {
|
->whereHas('company', function ($query) {
|
||||||
$query->where('is_disabled',0);
|
$query->where('is_disabled',0);
|
||||||
})
|
})
|
||||||
|
->orderBy('id', 'DESC')
|
||||||
->with('company');
|
->with('company');
|
||||||
|
|
||||||
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill db = {$db}");
|
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill db = {$db}");
|
||||||
@ -112,15 +116,23 @@ class AutoBillCron
|
|||||||
->whereHas('company', function ($query) {
|
->whereHas('company', function ($query) {
|
||||||
$query->where('is_disabled',0);
|
$query->where('is_disabled',0);
|
||||||
})
|
})
|
||||||
|
->orderBy('id', 'DESC')
|
||||||
->with('company');
|
->with('company');
|
||||||
|
|
||||||
nlog($auto_bill_invoices->count(). " full invoices to auto bill db = {$db}");
|
nlog($auto_bill_invoices->count(). " full invoices to auto bill db = {$db}");
|
||||||
|
|
||||||
$auto_bill_invoices->cursor()->each(function ($invoice) use($db){
|
$auto_bill_invoices->cursor()->each(function ($invoice) use($db){
|
||||||
|
|
||||||
|
nlog($this->counter);
|
||||||
AutoBill::dispatch($invoice, $db);
|
AutoBill::dispatch($invoice, $db);
|
||||||
|
$this->counter++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nlog("Auto Bill - fine");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,15 +59,15 @@ class SendRecurring implements ShouldQueue
|
|||||||
public function handle() : void
|
public function handle() : void
|
||||||
{
|
{
|
||||||
//reset all contacts here
|
//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 = $invitation->contact;
|
||||||
$contact->send_email = true;
|
// $contact->send_email = true;
|
||||||
$contact->save();
|
// $contact->save();
|
||||||
|
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Generate Standard Invoice
|
// Generate Standard Invoice
|
||||||
$invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client);
|
$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.
|
//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]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ class Gateway extends StaticModel
|
|||||||
GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true],
|
GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true],
|
||||||
GatewayType::KBC => ['refund' => false, 'token_billing' => false],
|
GatewayType::KBC => ['refund' => false, 'token_billing' => false],
|
||||||
GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false],
|
GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false],
|
||||||
|
GatewayType::IDEAL => ['refund' => false, 'token_billing' => false],
|
||||||
];
|
];
|
||||||
case 15:
|
case 15:
|
||||||
return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal
|
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
|
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], //Square
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
case 52:
|
||||||
|
return [
|
||||||
|
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true] // GoCardless
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
|
@ -27,6 +27,7 @@ class GatewayType extends StaticModel
|
|||||||
const CREDIT = 10;
|
const CREDIT = 10;
|
||||||
const KBC = 11;
|
const KBC = 11;
|
||||||
const BANCONTACT = 12;
|
const BANCONTACT = 12;
|
||||||
|
const IDEAL = 13;
|
||||||
|
|
||||||
public function gateway()
|
public function gateway()
|
||||||
{
|
{
|
||||||
@ -63,7 +64,8 @@ class GatewayType extends StaticModel
|
|||||||
return ctrans('texts.kbc_cbc');
|
return ctrans('texts.kbc_cbc');
|
||||||
case self::BANCONTACT:
|
case self::BANCONTACT:
|
||||||
return ctrans('texts.bancontact');
|
return ctrans('texts.bancontact');
|
||||||
|
case self::IDEAL:
|
||||||
|
return ctrans('texts.ideal');
|
||||||
default:
|
default:
|
||||||
return 'Undefined.';
|
return 'Undefined.';
|
||||||
break;
|
break;
|
||||||
|
@ -45,6 +45,7 @@ class PaymentType extends StaticModel
|
|||||||
const MOLLIE_BANK_TRANSFER = 34;
|
const MOLLIE_BANK_TRANSFER = 34;
|
||||||
const KBC = 35;
|
const KBC = 35;
|
||||||
const BANCONTACT = 36;
|
const BANCONTACT = 36;
|
||||||
|
const IDEAL = 37;
|
||||||
|
|
||||||
public static function parseCardType($cardName)
|
public static function parseCardType($cardName)
|
||||||
{
|
{
|
||||||
|
@ -73,6 +73,7 @@ class SystemLog extends Model
|
|||||||
const TYPE_MOLLIE = 312;
|
const TYPE_MOLLIE = 312;
|
||||||
const TYPE_EWAY = 313;
|
const TYPE_EWAY = 313;
|
||||||
const TYPE_SQUARE = 320;
|
const TYPE_SQUARE = 320;
|
||||||
|
const TYPE_GOCARDLESS = 321;
|
||||||
|
|
||||||
const TYPE_QUOTA_EXCEEDED = 400;
|
const TYPE_QUOTA_EXCEEDED = 400;
|
||||||
const TYPE_UPSTREAM_FAILURE = 401;
|
const TYPE_UPSTREAM_FAILURE = 401;
|
||||||
|
@ -395,7 +395,7 @@ class BaseDriver extends AbstractPaymentDriver
|
|||||||
|
|
||||||
$invoices->first()->invitations->each(function ($invitation) use ($nmo) {
|
$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;
|
$nmo->to_user = $invitation->contact;
|
||||||
NinjaMailerJob::dispatch($nmo);
|
NinjaMailerJob::dispatch($nmo);
|
||||||
@ -459,7 +459,7 @@ class BaseDriver extends AbstractPaymentDriver
|
|||||||
|
|
||||||
$invoices->first()->invitations->each(function ($invitation) use ($nmo){
|
$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;
|
$nmo->to_user = $invitation->contact;
|
||||||
NinjaMailerJob::dispatch($nmo);
|
NinjaMailerJob::dispatch($nmo);
|
||||||
@ -492,81 +492,81 @@ class BaseDriver extends AbstractPaymentDriver
|
|||||||
public function checkRequirements()
|
public function checkRequirements()
|
||||||
{
|
{
|
||||||
if ($this->company_gateway->require_billing_address) {
|
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';
|
$this->required_fields[] = 'billing_address1';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->checkRequiredResource(auth()->user('contact')->client->address2)) {
|
if ($this->checkRequiredResource($this->client->address2)) {
|
||||||
$this->required_fields[] = 'billing_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';
|
$this->required_fields[] = 'billing_city';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->checkRequiredResource(auth()->user('contact')->client->state)) {
|
if ($this->checkRequiredResource($this->client->state)) {
|
||||||
$this->required_fields[] = 'billing_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';
|
$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';
|
$this->required_fields[] = 'billing_country';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->company_gateway->require_shipping_address) {
|
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';
|
$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';
|
$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';
|
$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';
|
$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';
|
$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';
|
$this->required_fields[] = 'shipping_country';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->company_gateway->require_client_name) {
|
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';
|
$this->required_fields[] = 'name';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->company_gateway->require_client_phone) {
|
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';
|
$this->required_fields[] = 'phone';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->company_gateway->require_contact_email) {
|
if ($this->company_gateway->require_contact_email) {
|
||||||
if ($this->checkRequiredResource(auth()->user('contact')->email)) {
|
if ($this->checkRequiredResource($this->email)) {
|
||||||
$this->required_fields[] = 'contact_email';
|
$this->required_fields[] = 'contact_email';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->company_gateway->require_contact_name) {
|
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';
|
$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';
|
$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';
|
$this->required_fields[] = 'postal_code';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
256
app/PaymentDrivers/GoCardless/ACH.php
Normal file
256
app/PaymentDrivers/GoCardless/ACH.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
253
app/PaymentDrivers/GoCardlessPaymentDriver.php
Normal file
253
app/PaymentDrivers/GoCardlessPaymentDriver.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
215
app/PaymentDrivers/Mollie/IDEAL.php
Normal file
215
app/PaymentDrivers/Mollie/IDEAL.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ use App\Models\SystemLog;
|
|||||||
use App\PaymentDrivers\Mollie\Bancontact;
|
use App\PaymentDrivers\Mollie\Bancontact;
|
||||||
use App\PaymentDrivers\Mollie\BankTransfer;
|
use App\PaymentDrivers\Mollie\BankTransfer;
|
||||||
use App\PaymentDrivers\Mollie\CreditCard;
|
use App\PaymentDrivers\Mollie\CreditCard;
|
||||||
|
use App\PaymentDrivers\Mollie\IDEAL;
|
||||||
use App\PaymentDrivers\Mollie\KBC;
|
use App\PaymentDrivers\Mollie\KBC;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
@ -70,6 +71,7 @@ class MolliePaymentDriver extends BaseDriver
|
|||||||
GatewayType::BANCONTACT => Bancontact::class,
|
GatewayType::BANCONTACT => Bancontact::class,
|
||||||
GatewayType::BANK_TRANSFER => BankTransfer::class,
|
GatewayType::BANK_TRANSFER => BankTransfer::class,
|
||||||
GatewayType::KBC => KBC::class,
|
GatewayType::KBC => KBC::class,
|
||||||
|
GatewayType::IDEAL => IDEAL::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE;
|
const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE;
|
||||||
@ -93,6 +95,7 @@ class MolliePaymentDriver extends BaseDriver
|
|||||||
$types[] = GatewayType::BANCONTACT;
|
$types[] = GatewayType::BANCONTACT;
|
||||||
$types[] = GatewayType::BANK_TRANSFER;
|
$types[] = GatewayType::BANK_TRANSFER;
|
||||||
$types[] = GatewayType::KBC;
|
$types[] = GatewayType::KBC;
|
||||||
|
$types[] = GatewayType::IDEAL;
|
||||||
|
|
||||||
return $types;
|
return $types;
|
||||||
}
|
}
|
||||||
|
@ -309,10 +309,6 @@ class BaseRepository
|
|||||||
|
|
||||||
/* Perform model specific tasks */
|
/* Perform model specific tasks */
|
||||||
if ($model instanceof Invoice) {
|
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)) {
|
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class SendEmail
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->credit->invitations->each(function ($invitation) {
|
$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);
|
$email_builder = (new CreditEmail())->build($invitation, $this->reminder_template);
|
||||||
|
|
||||||
// EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company);
|
// EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company);
|
||||||
|
@ -23,6 +23,7 @@ use App\Models\PaymentType;
|
|||||||
use App\Services\AbstractService;
|
use App\Services\AbstractService;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
class AutoBillInvoice extends AbstractService
|
class AutoBillInvoice extends AbstractService
|
||||||
{
|
{
|
||||||
@ -113,10 +114,17 @@ class AutoBillInvoice extends AbstractService
|
|||||||
|
|
||||||
nlog("Payment hash created => {$payment_hash->id}");
|
nlog("Payment hash created => {$payment_hash->id}");
|
||||||
|
|
||||||
|
$payment = false;
|
||||||
|
|
||||||
|
try{
|
||||||
$payment = $gateway_token->gateway
|
$payment = $gateway_token->gateway
|
||||||
->driver($this->client)
|
->driver($this->client)
|
||||||
->setPaymentHash($payment_hash)
|
->setPaymentHash($payment_hash)
|
||||||
->tokenBilling($gateway_token, $payment_hash);
|
->tokenBilling($gateway_token, $payment_hash);
|
||||||
|
}
|
||||||
|
catch(\Exception $e){
|
||||||
|
nlog($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
if($payment){
|
if($payment){
|
||||||
info("Auto Bill payment captured for ".$this->invoice->number);
|
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
|
//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;
|
// $gateway_tokens = $this->client->gateway_tokens;
|
||||||
|
|
||||||
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use($amount) {
|
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use($amount) {
|
||||||
@ -304,7 +312,7 @@ class AutoBillInvoice extends AbstractService
|
|||||||
$company_gateway = $gateway_token->gateway;
|
$company_gateway = $gateway_token->gateway;
|
||||||
|
|
||||||
//check if fees and limits are set
|
//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 valid we keep this gateway_token
|
||||||
if ($this->invoice->client->validGatewayForAmount($company_gateway->fees_and_limits->{$gateway_token->gateway_type_id}, $amount))
|
if ($this->invoice->client->validGatewayForAmount($company_gateway->fees_and_limits->{$gateway_token->gateway_type_id}, $amount))
|
||||||
|
@ -44,7 +44,7 @@ class SendEmail extends AbstractService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->invoice->invitations->each(function ($invitation) {
|
$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);
|
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -33,7 +33,7 @@ class SendEmail
|
|||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
$this->payment->client->contacts->each(function ($contact) {
|
$this->payment->client->contacts->each(function ($contact) {
|
||||||
if ($contact->send_email && $contact->email) {
|
if ($contact->email) {
|
||||||
EmailPayment::dispatchNow($this->payment, $this->payment->company, $contact);
|
EmailPayment::dispatchNow($this->payment, $this->payment->company, $contact);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -77,8 +77,6 @@ class PdfMaker
|
|||||||
$this->updateVariables($this->data['variables']);
|
$this->updateVariables($this->data['variables']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->processOptions();
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,144 +163,6 @@ trait PdfMakerUtilities
|
|||||||
return $element;
|
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)
|
public function getEmptyElements(array &$elements, array $variables)
|
||||||
{
|
{
|
||||||
foreach ($elements as &$element) {
|
foreach ($elements as &$element) {
|
||||||
|
@ -39,7 +39,7 @@ class ConvertQuote
|
|||||||
{
|
{
|
||||||
$invoice = CloneQuoteToInvoiceFactory::create($quote, $quote->user_id);
|
$invoice = CloneQuoteToInvoiceFactory::create($quote, $quote->user_id);
|
||||||
$invoice->design_id = $this->decodePrimaryKey($this->client->getSetting('invoice_design_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();
|
$invoice->fresh();
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class SendEmail
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->quote->invitations->each(function ($invitation) {
|
$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);
|
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -129,6 +129,9 @@ class HtmlEngine
|
|||||||
$data['$invoice.datetime'] = &$data['$entity.datetime'];
|
$data['$invoice.datetime'] = &$data['$entity.datetime'];
|
||||||
$data['$quote.datetime'] = &$data['$entity.datetime'];
|
$data['$quote.datetime'] = &$data['$entity.datetime'];
|
||||||
$data['$credit.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') {
|
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
|
||||||
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')];
|
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')];
|
||||||
@ -140,7 +143,7 @@ class HtmlEngine
|
|||||||
$data['$viewLink'] = &$data['$view_link'];
|
$data['$viewLink'] = &$data['$view_link'];
|
||||||
$data['$viewButton'] = &$data['$view_link'];
|
$data['$viewButton'] = &$data['$view_link'];
|
||||||
$data['$view_button'] = &$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['$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()) ?: ' ', 'label' => ctrans('texts.invoice_date')];
|
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: ' ', 'label' => ctrans('texts.invoice_date')];
|
||||||
|
|
||||||
@ -457,7 +460,7 @@ class HtmlEngine
|
|||||||
$data['$auto_bill'] = &$data['$autoBill'];
|
$data['$auto_bill'] = &$data['$autoBill'];
|
||||||
|
|
||||||
/*Payment Aliases*/
|
/*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['$portalButton'] = &$data['$paymentLink'];
|
||||||
|
|
||||||
$data['$dir'] = ['value' => optional($this->client->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];
|
$data['$dir'] = ['value' => optional($this->client->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];
|
||||||
|
@ -82,11 +82,20 @@ class Phantom
|
|||||||
$url = config('ninja.app_url').'/phantom/'.$entity.'/'.$invitation->key.'?phantomjs_secret='.config('ninja.phantomjs_secret');
|
$url = config('ninja.app_url').'/phantom/'.$entity.'/'.$invitation->key.'?phantomjs_secret='.config('ninja.phantomjs_secret');
|
||||||
info($url);
|
info($url);
|
||||||
|
|
||||||
$key = config('ninja.phantomjs_key');
|
$key = config( 'ninja.phantomjs_key' );
|
||||||
$secret = config('ninja.phantomjs_key');
|
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/";
|
||||||
|
$pdf = CurlUtils::post( $phantom_url, json_encode( [
|
||||||
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D";
|
'url' => $url,
|
||||||
$pdf = CurlUtils::get($phantom_url);
|
'renderType' => 'pdf',
|
||||||
|
'outputAsJson' => false,
|
||||||
|
'renderSettings' => [
|
||||||
|
'emulateMedia' => 'print',
|
||||||
|
'pdfOptions' => [
|
||||||
|
'preferCSSPageSize' => true,
|
||||||
|
'printBackground' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] ) );
|
||||||
|
|
||||||
$this->checkMime($pdf, $invitation, $entity);
|
$this->checkMime($pdf, $invitation, $entity);
|
||||||
|
|
||||||
@ -100,14 +109,20 @@ class Phantom
|
|||||||
|
|
||||||
public function convertHtmlToPdf($html)
|
public function convertHtmlToPdf($html)
|
||||||
{
|
{
|
||||||
$hash = Str::random(32);
|
$key = config( 'ninja.phantomjs_key' );
|
||||||
Cache::put($hash, $html, 300);
|
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/";
|
||||||
|
$pdf = CurlUtils::post( $phantom_url, json_encode( [
|
||||||
$url = route('tmp_pdf', ['hash' => $hash]);
|
'content' => $html,
|
||||||
info($url);
|
'renderType' => 'pdf',
|
||||||
$key = config('ninja.phantomjs_key');
|
'outputAsJson' => false,
|
||||||
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D";
|
'renderSettings' => [
|
||||||
$pdf = CurlUtils::get($phantom_url);
|
'emulateMedia' => 'print',
|
||||||
|
'pdfOptions' => [
|
||||||
|
'preferCSSPageSize' => true,
|
||||||
|
'printBackground' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] ) );
|
||||||
|
|
||||||
$response = Response::make($pdf, 200);
|
$response = Response::make($pdf, 200);
|
||||||
$response->header('Content-Type', 'application/pdf');
|
$response->header('Content-Type', 'application/pdf');
|
||||||
|
@ -43,6 +43,17 @@ trait Inviteable
|
|||||||
return $status;
|
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
|
public function getLink() :string
|
||||||
{
|
{
|
||||||
$entity_type = Str::snake(class_basename($this->entityType()));
|
$entity_type = Str::snake(class_basename($this->entityType()));
|
||||||
|
@ -270,7 +270,7 @@ trait MakesInvoiceValues
|
|||||||
if (! is_array($items)) {
|
if (! is_array($items)) {
|
||||||
$data;
|
$data;
|
||||||
}
|
}
|
||||||
|
|
||||||
$locale_info = localeconv();
|
$locale_info = localeconv();
|
||||||
|
|
||||||
foreach ($items as $key => $item) {
|
foreach ($items as $key => $item) {
|
||||||
@ -296,11 +296,7 @@ trait MakesInvoiceValues
|
|||||||
|
|
||||||
$data[$key][$table_type.'.notes'] = Helpers::processReservedKeywords($item->notes, $this->client);
|
$data[$key][$table_type.'.notes'] = Helpers::processReservedKeywords($item->notes, $this->client);
|
||||||
$data[$key][$table_type.'.description'] = 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}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}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);
|
$data[$key][$table_type . ".{$_table_type}3"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}3", $item->custom_value3, $this->client);
|
||||||
@ -314,8 +310,12 @@ trait MakesInvoiceValues
|
|||||||
$data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $this->client);
|
$data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $this->client);
|
||||||
|
|
||||||
$data[$key][$table_type.'.line_total'] = Number::formatMoney($item->line_total, $this->client);
|
$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 (isset($item->discount) && $item->discount > 0) {
|
||||||
if ($item->is_amount_discount) {
|
if ($item->is_amount_discount) {
|
||||||
$data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $this->client);
|
$data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $this->client);
|
||||||
|
406
composer.lock
generated
406
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -14,8 +14,8 @@ return [
|
|||||||
'require_https' => env('REQUIRE_HTTPS', true),
|
'require_https' => env('REQUIRE_HTTPS', true),
|
||||||
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
||||||
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
||||||
'app_version' => '5.3.19',
|
'app_version' => '5.3.21',
|
||||||
'app_tag' => '5.3.19',
|
'app_tag' => '5.3.21',
|
||||||
'minimum_client_version' => '5.0.16',
|
'minimum_client_version' => '5.0.16',
|
||||||
'terms_version' => '1.0.1',
|
'terms_version' => '1.0.1',
|
||||||
'api_secret' => env('API_SECRET', ''),
|
'api_secret' => env('API_SECRET', ''),
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
@ -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' => 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' => 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' => 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' => 53, 'name' => 'PagSeguro', 'provider' => 'PagSeguro', 'key' => 'ef498756b54db63c143af0ec433da803', 'fields' => '{"email":"","token":"","sandbox":false}'],
|
||||||
['id' => 54, 'name' => 'PAYMILL', 'provider' => 'Paymill', 'key' => 'ca52f618a39367a4c944098ebf977e1c', 'fields' => '{"apiKey":""}'],
|
['id' => 54, 'name' => 'PAYMILL', 'provider' => 'Paymill', 'key' => 'ca52f618a39367a4c944098ebf977e1c', 'fields' => '{"apiKey":""}'],
|
||||||
['id' => 55, 'name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 21, 'key' => '54faab2ab6e3223dbe848b1686490baa', 'fields' => '{"name":"","text":""}'],
|
['id' => 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::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()) {
|
if (Ninja::isHosted()) {
|
||||||
Gateway::whereIn('id', [20])->update(['visible' => 0]);
|
Gateway::whereIn('id', [20])->update(['visible' => 0]);
|
||||||
|
4
public/flutter_service_worker.js
vendored
4
public/flutter_service_worker.js
vendored
@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
|
|||||||
const CACHE_NAME = 'flutter-app-cache';
|
const CACHE_NAME = 'flutter-app-cache';
|
||||||
const RESOURCES = {
|
const RESOURCES = {
|
||||||
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
|
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
|
||||||
"/": "0d9690c2b925c794b94e0778817e5c19",
|
"/": "c89483e4d5b7e4169b05f1bc0cbe5935",
|
||||||
"assets/NOTICES": "9eb7e2eb2888ea5bae5f536720db37cd",
|
"assets/NOTICES": "9eb7e2eb2888ea5bae5f536720db37cd",
|
||||||
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
|
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
|
||||||
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
|
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
|
||||||
@ -34,7 +34,7 @@ const RESOURCES = {
|
|||||||
"favicon.ico": "51636d3a390451561744c42188ccd628",
|
"favicon.ico": "51636d3a390451561744c42188ccd628",
|
||||||
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
|
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
|
||||||
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
|
"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
|
// The application shell files that are downloaded before a service worker can
|
||||||
|
113168
public/main.dart.js
vendored
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
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
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
119642
public/main.next.dart.js
vendored
File diff suppressed because one or more lines are too long
8317
public/main.profile.dart.js
vendored
8317
public/main.profile.dart.js
vendored
File diff suppressed because one or more lines are too long
110488
public/main.wasm.dart.js
vendored
110488
public/main.wasm.dart.js
vendored
File diff suppressed because one or more lines are too long
@ -4316,6 +4316,7 @@ $LANG = array(
|
|||||||
'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.',
|
'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.',
|
||||||
'kbc_cbc' => 'KBC/CBC',
|
'kbc_cbc' => 'KBC/CBC',
|
||||||
'bancontact' => 'Bancontact',
|
'bancontact' => 'Bancontact',
|
||||||
|
'ideal' => 'iDEAL',
|
||||||
);
|
);
|
||||||
|
|
||||||
return $LANG;
|
return $LANG;
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"content": {
|
"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.",
|
"message": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"dismiss": "Got it!",
|
"dismiss": "Got it!",
|
||||||
"link": "Learn more",
|
"link": "Learn more",
|
||||||
|
@ -13,31 +13,51 @@
|
|||||||
zoom: 80%;
|
zoom: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@page {
|
@page {
|
||||||
margin: -0.25cm !important;
|
margin: 0 !important;
|
||||||
size: $page_size $page_layout;
|
size: $page_size $page_layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
/* page-break-after: always; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-wrapper {
|
#spacer-table > * > tr > td {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spacer-table{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 1fr 1fr;
|
grid-template-columns: 1.5fr 1fr 1fr;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
background-color: #2d2c2a;
|
background-color: #2d2c2a;
|
||||||
padding: 3rem;
|
|
||||||
color: white;
|
color: white;
|
||||||
min-width: 100%;
|
|
||||||
line-height: var(--line-height);
|
line-height: var(--line-height);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#header, #header-spacer {
|
||||||
|
height: 160px;
|
||||||
|
padding: 3rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
.company-logo {
|
.company-logo {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-right: 120px;
|
max-width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#company-details,
|
#company-details,
|
||||||
@ -46,6 +66,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#company-details,
|
||||||
|
#company-address,
|
||||||
|
.logo-container {
|
||||||
|
max-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
#client-details {
|
#client-details {
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -61,7 +87,6 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 1fr;
|
grid-template-columns: 1.5fr 1fr;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-top: 3rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-details-wrapper {
|
.entity-details-wrapper {
|
||||||
@ -86,7 +111,11 @@
|
|||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
/* margin-bottom: 200px; */
|
margin-bottom: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-ref="table"]:last-child{
|
||||||
|
margin-bottom:0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-time-details {
|
.task-time-details {
|
||||||
@ -138,23 +167,23 @@
|
|||||||
gap: 80px;
|
gap: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#table-totals .totals-table-right-side>* {
|
#table-totals .totals-table-right-side > * {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
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";
|
text-align: "$dir_text_align";
|
||||||
margin-top: .75rem;
|
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;
|
--tw-space-y-reverse: 0;
|
||||||
margin-top: calc(.75rem * calc(1 - var(--tw-space-y-reverse)));
|
margin-top: calc(.75rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
margin-bottom: calc(.75rem * 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;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,51 +215,40 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-wrapper {
|
#footer {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
background-color: #2d2c2a;
|
background-color: #2d2c2a;
|
||||||
height: 160px;
|
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 1rem 3rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#footer, #footer-spacer {
|
||||||
|
height: 160px;
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
[data-ref="total_table-footer"] {
|
[data-ref="total_table-footer"] {
|
||||||
padding-top: 0.5rem
|
padding-top: 0.5rem
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Repeating header & footer styling. */
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.print-content {}
|
table[data-ref="table"] th,
|
||||||
|
table[data-ref="table"] td {
|
||||||
table.print-content th,
|
|
||||||
table.print-content td {
|
|
||||||
padding: .2rem .4rem;
|
padding: .2rem .4rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
border-top: 1px solid #dee2e6;
|
border-top: 1px solid #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
|
||||||
.print-footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-print {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Markdown-specific styles. **/
|
/** Markdown-specific styles. **/
|
||||||
#product-table h3,
|
#product-table h3,
|
||||||
#task-table h3,
|
#task-table h3,
|
||||||
@ -239,9 +257,9 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-ref="product_table-product.description-th"] {
|
[data-ref="product_table-product.description-th"] {
|
||||||
width: 23%;
|
width: 23%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-ref="statement-totals"] {
|
[data-ref="statement-totals"] {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
@ -253,10 +271,6 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container {
|
|
||||||
max-height: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#statement-invoice-table-totals > p {
|
#statement-invoice-table-totals > p {
|
||||||
margin-right: 2rem;
|
margin-right: 2rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
@ -264,101 +278,94 @@
|
|||||||
|
|
||||||
/** Useful snippets, uncomment to enable. **/
|
/** Useful snippets, uncomment to enable. **/
|
||||||
|
|
||||||
/** Hide company logo **/
|
/** Hide company logo **/
|
||||||
/* .company-logo { display: none } */
|
/* .company-logo { display: none } */
|
||||||
|
|
||||||
/* Hide company details */
|
/* Hide company details */
|
||||||
/* #company-details > * { display: none } */
|
/* #company-details > * { display: none } */
|
||||||
|
|
||||||
/* Hide company address */
|
/* Hide company address */
|
||||||
/* #company-address > * { display: none } */
|
/* #company-address > * { display: none } */
|
||||||
|
|
||||||
/* Hide public notes */
|
/* Hide public notes */
|
||||||
/* [data-ref="total_table-public_notes"] { display: none } */
|
/* [data-ref="total_table-public_notes"] { display: none } */
|
||||||
|
|
||||||
/* Hide terms label */
|
/* Hide terms label */
|
||||||
/* [data-ref="total_table-terms-label"] { display: none } */
|
/* [data-ref="total_table-terms-label"] { display: none } */
|
||||||
|
|
||||||
/* Hide totals table */
|
/* Hide totals table */
|
||||||
/* #table-totals { display: none } */
|
/* #table-totals { display: none } */
|
||||||
|
|
||||||
/* Hide totals table left side */
|
/* Hide totals table left side */
|
||||||
/* #table-totals div:first-child > * { display: none !important } */
|
/* #table-totals div:first-child > * { display: none !important } */
|
||||||
|
|
||||||
/* Hide totals table right side */
|
/* Hide totals table right side */
|
||||||
/* .totals-table-right-side { display: none } */
|
/* .totals-table-right-side { display: none } */
|
||||||
|
|
||||||
/** For more info, please check our docs: https://invoiceninja.github.io **/
|
/** 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 **/
|
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<table>
|
<div id="header">
|
||||||
<!-- Start Header -->
|
<div class="logo-container">
|
||||||
<thead>
|
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
|
||||||
<tr>
|
</div>
|
||||||
<td>
|
<div id="company-details"></div>
|
||||||
<div class="header-wrapper" id="header">
|
<div id="company-address"></div>
|
||||||
<div class="logo-container">
|
</div>
|
||||||
<img class="company-logo" src="$company.logo" alt="$company.name logo"/>
|
<div id="body">
|
||||||
</div>
|
<table id="spacer-table" cellspacing="0" >
|
||||||
<div id="company-details"></div>
|
<thead>
|
||||||
<div id="company-address"></div>
|
<tr>
|
||||||
</div>
|
<td>
|
||||||
</td>
|
<div id="header-spacer"></div>
|
||||||
</tr>
|
</td>
|
||||||
</thead>
|
</tr>
|
||||||
<!-- End Header -->
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td id="body">
|
<tr>
|
||||||
<div class="client-entity-wrapper">
|
<td>
|
||||||
<div class="client-wrapper-left-side">
|
<div class="client-entity-wrapper">
|
||||||
<h4 class="entity-label">$entity_label</h4>
|
<div class="client-wrapper-left-side">
|
||||||
<div id="client-details" cellspacing="0"></div>
|
<h4 class="entity-label">$entity_label</h4>
|
||||||
</div>
|
<div id="client-details" cellspacing="0"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="entity-details-wrapper-right-side">
|
<div class="entity-details-wrapper-right-side">
|
||||||
<div class="entity-details-wrapper">
|
<div class="entity-details-wrapper">
|
||||||
<table id="entity-details" dir="$dir"></table>
|
<table id="entity-details" dir="$dir"></table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Start Print Content -->
|
<table id="product-table" cellspacing="0" data-ref="table"></table>
|
||||||
<table id="product-table" cellspacing="0" class="print-content" 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>
|
<table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
|
||||||
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
|
<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>
|
<table id="statement-payment-table" cellspacing="0" data-ref="table"></table>
|
||||||
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
|
<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>
|
<table id="statement-aging-table" cellspacing="0" data-ref="table"></table>
|
||||||
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
|
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
|
||||||
<!-- End Print Content -->
|
<div id="table-totals" cellspacing="0"></div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
</tbody>
|
||||||
<td>
|
<tfoot>
|
||||||
<div id="table-totals" cellspacing="0"></div>
|
<tr>
|
||||||
</td>
|
<td>
|
||||||
</tr>
|
<div id="footer-spacer"></div>
|
||||||
<!-- Start Space For Footer -->
|
</td>
|
||||||
<tfoot>
|
</tr>
|
||||||
<tr>
|
</tfoot>
|
||||||
<td style="height: 180px">
|
</table>
|
||||||
<!-- Leave this empty and don't remove it. This space is where footer placed on print. -->
|
</div>
|
||||||
</td>
|
<div id="footer">
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
<!-- End Space For Footer -->
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Start Footer -->
|
|
||||||
<div class="footer-wrapper print-footer" id="footer">
|
|
||||||
<div>
|
<div>
|
||||||
<p data-ref="total_table-footer">$entity_footer</p>
|
<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.
|
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
let tables = [
|
let tables = [
|
||||||
'product-table', 'task-table', 'delivery-note-table',
|
'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-totals',
|
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table', 'statement-aging-table-totals', 'statement-payment-table-totals'
|
||||||
];
|
];
|
||||||
|
|
||||||
tables.forEach((tableIdentifier) => {
|
tables.forEach((tableIdentifier) => {
|
||||||
document.getElementById(tableIdentifier).childElementCount === 0
|
const el =document.getElementById(tableIdentifier);
|
||||||
? document.getElementById(tableIdentifier).style.display = 'none'
|
if(el && el.childElementCount === 0)el.remove()
|
||||||
: '';
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -381,4 +387,3 @@
|
|||||||
<div> <!-- #2 column --> </div>
|
<div> <!-- #2 column --> </div>
|
||||||
<div> <!-- #3 column --> </div>
|
<div> <!-- #3 column --> </div>
|
||||||
</div>
|
</div>
|
||||||
<!-- End Footer -->
|
|
||||||
|
@ -9,38 +9,54 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
font-size: "7px";
|
font-size: "7px";
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
zoom: 80%;
|
zoom: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@page {
|
@page {
|
||||||
margin: -0.22cm;
|
margin: 0 !important;
|
||||||
size: A4 portrait;
|
size: $page_size $page_layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
/* page-break-after: always; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-container {
|
#spacer-table > * > tr > td {
|
||||||
background-color: var(--primary-color);
|
padding: 0;
|
||||||
color: white;
|
}
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1.5fr 1fr;
|
#spacer-table{
|
||||||
padding: 3rem;
|
width: 100%;
|
||||||
min-width: 100%;
|
}
|
||||||
height: 160px;
|
|
||||||
|
#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 {
|
.company-name {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-container .company-name {
|
#header .company-name {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#entity-details {
|
#entity-details {
|
||||||
@ -55,7 +71,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo-client-wrapper {
|
.logo-client-wrapper {
|
||||||
margin: 3rem 2rem;
|
margin: 0 2rem 3rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 1fr;
|
grid-template-columns: 1.5fr 1fr;
|
||||||
}
|
}
|
||||||
@ -121,15 +137,18 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-wrapper {
|
#footer {
|
||||||
margin-top: 1rem;
|
background-color: var(--primary-color);
|
||||||
background-color: var(--primary-color);
|
width: 100%;
|
||||||
padding-left: 3rem;
|
position: fixed;
|
||||||
padding-right: 3rem;
|
bottom: 0;
|
||||||
height: 220px;
|
}
|
||||||
width: 100%;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
#footer, #footer-spacer {
|
||||||
|
height: 220px;
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-content {
|
.footer-content {
|
||||||
@ -204,54 +223,21 @@
|
|||||||
font-size: 1.3rem;
|
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"] {
|
[data-ref="total_table-footer"] {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Repeating header & footer styling. */
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.print-content {
|
table[data-ref="table"] th,
|
||||||
}
|
table[data-ref="table"] td {
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
table.print-content th,
|
text-align: left;
|
||||||
table.print-content td {
|
vertical-align: top;
|
||||||
padding: 0.2rem 0.4rem;
|
border-top: 1px solid #dee2e6;
|
||||||
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. **/
|
/** Markdown-specific styles. **/
|
||||||
@ -306,70 +292,57 @@
|
|||||||
/** For more info, please check our docs: https://invoiceninja.github.io **/
|
/** 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 **/
|
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
|
||||||
</style>
|
</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>
|
<table id="task-table" cellspacing="0" data-ref="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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div id="client-details"></div>
|
<table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Start Print Content -->
|
<table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
|
||||||
<div class="table-wrapper">
|
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
|
||||||
<table id="product-table" cellspacing="0" class="print-content" data-ref="table"></table>
|
|
||||||
|
|
||||||
<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="footer">
|
||||||
<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 class="footer-content">
|
<div class="footer-content">
|
||||||
<div>
|
<div>
|
||||||
<p data-ref="total_table-footer">$entity_footer</p>
|
<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.
|
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
let tables = [
|
let tables = [
|
||||||
'product-table', 'task-table', 'delivery-note-table',
|
'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-totals',
|
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table', 'statement-aging-table-totals', 'statement-payment-table-totals'
|
||||||
];
|
];
|
||||||
|
|
||||||
tables.forEach((tableIdentifier) => {
|
tables.forEach((tableIdentifier) => {
|
||||||
document.getElementById(tableIdentifier).childElementCount === 0
|
document.getElementById(tableIdentifier).childElementCount === 0
|
||||||
? document.getElementById(tableIdentifier).style.display = 'none'
|
? document.getElementById(tableIdentifier).remove()
|
||||||
: '';
|
: '';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -396,4 +369,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- End Footer -->
|
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -12,14 +12,90 @@
|
|||||||
|
|
||||||
<form action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{ more: false }">
|
<form action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{ more: false }">
|
||||||
@csrf
|
@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>
|
||||||
|
|
||||||
|
@if($field['required'])
|
||||||
|
<section class="text-red-400 ml-1 text-sm">*</section>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
|
|
||||||
<div x-show="more">
|
@if($field['key'] === 'email')
|
||||||
@include('portal.ninja2020.auth.includes.register.website')
|
<input
|
||||||
@include('portal.ninja2020.auth.includes.register.personal_address')
|
id="{{ $field['key'] }}"
|
||||||
@include('portal.ninja2020.auth.includes.register.shipping_address')
|
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>
|
||||||
|
|
||||||
<div class="flex justify-between items-center mt-8">
|
<div class="flex justify-between items-center mt-8">
|
||||||
|
@ -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
|
@ -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
|
@ -114,7 +114,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"content": {
|
"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.",
|
"message": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"dismiss": "Got it!",
|
"dismiss": "Got it!",
|
||||||
"link": "Learn more",
|
"link": "Learn more",
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"content": {
|
"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.",
|
"message": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"dismiss": "Got it!",
|
"dismiss": "Got it!",
|
||||||
"link": "Learn more",
|
"link": "Learn more",
|
||||||
|
@ -107,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"content": {
|
"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.",
|
"message": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"dismiss": "Got it!",
|
"dismiss": "Got it!",
|
||||||
"link": "Learn more",
|
"link": "Learn more",
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<input type="checkbox" class="form-checkbox mr-1"
|
<input type="checkbox" class="form-checkbox mr-1"
|
||||||
name="send_logs" {{ old('send_logs' ? 'checked': '') }}>
|
name="send_logs" {{ old('send_logs' ? 'checked': '') }}>
|
||||||
<span>{{ ctrans('texts.send_fail_logs_to_our_server') }}</span>
|
<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>
|
about how we use this.</a>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,12 @@
|
|||||||
<div class="col-span-12 md:col-start-4 md:col-span-6 mt-4 md:mt-10">
|
<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>
|
<h1 class="text-center text-2xl font-semibold">Invoice Ninja Setup</h1>
|
||||||
<p class="text-sm text-center">{{ ctrans('texts.if_you_need_help') }}
|
<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>
|
</p>
|
||||||
|
|
||||||
@if($errors->any())
|
@if($errors->any())
|
||||||
|
@ -95,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"content": {
|
"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.",
|
"message": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"dismiss": "Got it!",
|
"dismiss": "Got it!",
|
||||||
"link": "Learn more",
|
"link": "Learn more",
|
||||||
|
@ -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::get('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@verify')->name('payment_methods.verification');
|
||||||
Route::post('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@processVerification');
|
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::resource('payment_methods', 'ClientPortal\PaymentMethodController')->except(['edit', 'update']);
|
||||||
|
|
||||||
Route::match(['GET', 'POST'], 'quotes/approve', 'ClientPortal\QuoteController@bulk')->name('quotes.bulk');
|
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('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('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key');
|
||||||
Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
|
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
|
// Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe')->name('invoice.client_hash_and_invitation_key'); //should never need this
|
||||||
|
|
||||||
});
|
});
|
||||||
|
42
tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php
Normal file
42
tests/Browser/ClientPortal/Gateways/GoCardless/ACHTest.php
Normal 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.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
106
tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php
Normal file
106
tests/Browser/ClientPortal/Gateways/Mollie/IDEALTest.php
Normal 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.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -93,9 +93,9 @@ class CreditsTest extends TestCase
|
|||||||
$this->actingAs($client->contacts->first(), 'contact');
|
$this->actingAs($client->contacts->first(), 'contact');
|
||||||
|
|
||||||
Livewire::test(CreditsTable::class, ['company' => $company])
|
Livewire::test(CreditsTable::class, ['company' => $company])
|
||||||
->assertSee('testing-number-01')
|
->assertDontSee('testing-number-01')
|
||||||
->assertSee('testing-number-02')
|
->assertSee('testing-number-02')
|
||||||
->assertDontSee('testing-number-03');
|
->assertSee('testing-number-03');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testShowingCreditsWithNullDueDate()
|
public function testShowingCreditsWithNullDueDate()
|
||||||
@ -122,6 +122,7 @@ class CreditsTest extends TestCase
|
|||||||
'client_id' => $client->id,
|
'client_id' => $client->id,
|
||||||
'number' => 'testing-number-01',
|
'number' => 'testing-number-01',
|
||||||
'status_id' => Credit::STATUS_SENT,
|
'status_id' => Credit::STATUS_SENT,
|
||||||
|
'due_date' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Credit::factory()->create([
|
Credit::factory()->create([
|
||||||
@ -142,12 +143,21 @@ class CreditsTest extends TestCase
|
|||||||
'status_id' => Credit::STATUS_SENT,
|
'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');
|
$this->actingAs($client->contacts->first(), 'contact');
|
||||||
|
|
||||||
Livewire::test(CreditsTable::class, ['company' => $company])
|
Livewire::test(CreditsTable::class, ['company' => $company])
|
||||||
->assertSee('testing-number-01')
|
->assertSee('testing-number-01')
|
||||||
->assertSee('testing-number-02')
|
->assertSee('testing-number-02')
|
||||||
->assertDontSee('testing-number-03');
|
->assertSee('testing-number-03');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user