1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 13:12:50 +01:00

Merge pull request #7237 from turbo124/v5-develop

Allow duplicate Taxes to be created
This commit is contained in:
David Bomba 2022-02-26 16:09:05 +11:00 committed by GitHub
commit f89bb1c1af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1185 additions and 447 deletions

View File

@ -77,10 +77,10 @@ class BackupUpdate extends Command
{
set_time_limit(0);
Backup::whereRaw('html_backup != "" OR html_backup IS NOT NULL')->chunk(5000, function ($backups) {
Backup::whereRaw('html_backup IS NOT NULL')->chunk(5000, function ($backups) {
foreach ($backups as $backup) {
if($backup->activity->client()->exists()){
if(strlen($backup->html_backup) > 1 && $backup->activity->client()->exists()){
$client = $backup->activity->client;
$backup->storeRemotely($backup->html_backup, $client);

View File

@ -28,6 +28,7 @@ use App\Models\Payment;
use App\Models\Paymentable;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Models\Vendor;
use App\Utils\Ninja;
use Exception;
use Illuminate\Console\Command;
@ -295,6 +296,9 @@ class CheckData extends Command
}
if ($this->option('fix') == 'true') {
$vendors = Vendor::withTrashed()->doesntHave('contacts')->get();
foreach ($vendors as $vendor) {
$this->logMessage("Fixing missing vendor contacts #{$vendor->id}");

View File

@ -19,8 +19,10 @@ use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\QuoteInvitation;
use App\Services\ClientPortal\InstantPayment;
use App\Utils\CurlUtils;
use App\Utils\Ninja;
@ -265,4 +267,26 @@ class InvitationController extends Controller
abort(404, "Invoice not found");
}
public function unsubscribe(Request $request, string $invitation_key)
{
if($invite = InvoiceInvitation::withTrashed()->where('key', $invitation_key)->first()){
$invite->contact->send_email = false;
$invite->contact->save();
}elseif($invite = QuoteInvitation::withTrashed()->where('key', $invitation_key)->first()){
$invite->contact->send_email = false;
$invite->contact->save();
}elseif($invite = CreditInvitation::withTrashed()->where('key', $invitation_key)->first()){
$invite->contact->send_email = false;
$invite->contact->save();
}
else
return abort(404);
$data['logo'] = $invite->company->present()->logo();
return $this->render('generic.unsubscribe', $data);
}
}

View File

@ -12,15 +12,19 @@
namespace App\Http\Controllers\ClientPortal;
use App\Factory\RecurringInvoiceFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Uploads\StoreUploadRequest;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Models\Subscription;
use App\Repositories\SubscriptionRepository;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Routing\ResponseFactory;
@ -35,6 +39,7 @@ class NinjaPlanController extends Controller
public function index(string $contact_key, string $account_or_company_key)
{
MultiDB::findAndSetDbByCompanyKey($account_or_company_key);
$company = Company::where('company_key', $account_or_company_key)->first();
@ -67,8 +72,108 @@ class NinjaPlanController extends Controller
}
public function trial()
{
$gateway = CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->first();
$data['gateway'] = $gateway;
$gateway_driver = $gateway->driver(auth()->guard('contact')->user()->client)->init();
$customer = $gateway_driver->findOrCreateCustomer();
$setupIntent = \Stripe\SetupIntent::create([
'payment_method_types' => ['card'],
'usage' => 'off_session',
'customer' => $customer->id
]);
$data['intent'] = $setupIntent;
// $data['account'] = $account;
$data['client'] = Auth::guard('contact')->user()->client;
return $this->render('plan.trial', $data);
}
public function trial_confirmation(Request $request)
{
$client = auth()->guard('contact')->user()->client;
$client->fill($request->all());
$client->save();
//store payment method
$gateway = CompanyGateway::where('gateway_key', 'd14dd26a37cecc30fdd65700bfb55b23')->first();
$gateway_driver = $gateway->driver(auth()->guard('contact')->user()->client)->init();
$stripe_response = json_decode($request->input('gateway_response'));
$customer = $gateway_driver->findOrCreateCustomer();
$gateway_driver->attach($stripe_response->payment_method, $customer);
$method = $gateway_driver->getStripePaymentMethod($stripe_response->payment_method);
$payment_meta = new \stdClass;
$payment_meta->exp_month = (string) $method->card->exp_month;
$payment_meta->exp_year = (string) $method->card->exp_year;
$payment_meta->brand = (string) $method->card->brand;
$payment_meta->last4 = (string) $method->card->last4;
$payment_meta->type = GatewayType::CREDIT_CARD;
$data = [
'payment_meta' => $payment_meta,
'token' => $method->id,
'payment_method_id' => GatewayType::CREDIT_CARD,
];
$gateway_driver->storeGatewayToken($data, ['gateway_customer_reference' => $customer->id]);
//set free trial
$account = auth()->guard('contact')->user()->company->account;
$account->trial_started = now();
$account->trial_plan = 'pro';
$account->save();
//create recurring invoice
$subscription_repo = new SubscriptionRepository();
$subscription = Subscription::find(6);
$recurring_invoice = RecurringInvoiceFactory::create($subscription->company_id, $subscription->user_id);
$recurring_invoice->client_id = $client->id;
$recurring_invoice->line_items = $subscription_repo->generateLineItems($subscription, true, false);
$recurring_invoice->subscription_id = $subscription->id;
$recurring_invoice->frequency_id = $subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY;
$recurring_invoice->date = now()->addDays(14);
$recurring_invoice->remaining_cycles = -1;
$recurring_invoice->auto_bill = $client->getSetting('auto_bill');
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->due_date_days = 'terms';
$recurring_invoice->next_send_date = now()->addDays(14)->format('Y-m-d');
$recurring_invoice->save();
$recurring_invoice->service()->start();
return redirect('/');
}
private function setAutoBillFlag($auto_bill)
{
if ($auto_bill == 'always' || $auto_bill == 'optout') {
return true;
}
return false;
}
public function plan()
{
//harvest the current plan
$data = [];
$data['late_invoice'] = false;
@ -79,6 +184,9 @@ class NinjaPlanController extends Controller
if($account)
{
//offer the option to have a free trial
if(!$account->trial_started)
return $this->trial();
if(Carbon::parse($account->plan_expires)->lt(now())){
//expired get the most recent invoice for payment

View File

@ -112,7 +112,8 @@ class StripeConnectController extends BaseController
"livemode" => $response->livemode,
"stripe_user_id" => $response->stripe_user_id,
"refresh_token" => $response->refresh_token,
"access_token" => $response->access_token
"access_token" => $response->access_token,
"appleDomainVerification" => '',
];
$company_gateway->setConfig($payload);

View File

@ -29,7 +29,7 @@ class StoreTaxRateRequest extends Request
{
return [
//'name' => 'required',
'name' => 'required|unique:tax_rates,name,null,null,company_id,'.auth()->user()->companyId(),
'name' => 'required|unique:tax_rates,name,null,null,company_id,'.auth()->user()->companyId().',deleted_at,NULL',
'rate' => 'required|numeric',
];
}

View File

@ -78,10 +78,10 @@ class PaymentAmountsBalanceRule implements Rule
// nlog(request()->input('invoices'));
// nlog($payment_amounts);
// nlog($invoice_amounts);
// nlog(request()->all());
nlog($payment_amounts ." >= " . $invoice_amounts);
return $payment_amounts >= $invoice_amounts;
return round($payment_amounts,2) >= round($invoice_amounts,2);
}

View File

@ -84,9 +84,8 @@ class CreateAccount
{
$sp794f3f->hosted_client_count = config('ninja.quotas.free.clients');
$sp794f3f->hosted_company_count = config('ninja.quotas.free.max_companies');
$sp794f3f->trial_started = now();
$sp794f3f->trial_plan = 'pro';
// $sp794f3f->trial_started = now();
// $sp794f3f->trial_plan = 'pro';
}
$sp794f3f->save();

View File

@ -11,6 +11,8 @@
namespace App\Jobs\Import;
use App\Factory\ClientContactFactory;
use App\Factory\VendorContactFactory;
use App\Import\Providers\Csv;
use App\Import\Providers\Freshbooks;
use App\Import\Providers\Invoice2Go;
@ -18,12 +20,15 @@ use App\Import\Providers\Invoicely;
use App\Import\Providers\Wave;
use App\Import\Providers\Zoho;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\Vendor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
class CSVIngest implements ShouldQueue {
@ -70,6 +75,33 @@ class CSVIngest implements ShouldQueue {
}
$this->checkContacts();
}
private function checkContacts()
{
$vendors = Vendor::withTrashed()->where('company_id', $this->company->id)->doesntHave('contacts')->get();
foreach ($vendors as $vendor) {
$new_contact = VendorContactFactory::create($vendor->company_id, $vendor->user_id);
$new_contact->vendor_id = $vendor->id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->save();
}
$clients = Client::withTrashed()->where('company_id', $this->company->id)->doesntHave('contacts')->get();
foreach ($clients as $client) {
$new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
$new_contact->client_id = $client->id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->save();
}
}
private function bootEngine(string $import_type)

View File

@ -374,6 +374,9 @@ class Import implements ShouldQueue
$data['subdomain'] = MultiDB::randomSubdomainGenerator();
}
else {
$data['email_sending_method'] = 'default';
}
$rules = (new UpdateCompanyRequest())->rules();

View File

@ -97,6 +97,7 @@ class TemplateEmail extends Mailable
'footer' => $this->build_email->getFooter(),
'whitelabel' => $this->client->user->account->isPaid() ? true : false,
'settings' => $settings,
'unsubscribe_link' => $this->invitation->getUnsubscribeLink(),
])
->view($template_name, [
'greeting' => ctrans('texts.email_salutation', ['name' => $this->contact->present()->name()]),
@ -110,6 +111,7 @@ class TemplateEmail extends Mailable
'company' => $company,
'whitelabel' => $this->client->user->account->isPaid() ? true : false,
'logo' => $this->company->present()->logo($settings),
'unsubscribe_link' => $this->invitation->getUnsubscribeLink(),
])
->withSwiftMessage(function ($message) use($company){
$message->getHeaders()->addTextHeader('Tag', $company->company_key);

View File

@ -321,7 +321,6 @@ https://developer.wepay.com/api/api-calls/checkout
$amount = array_sum(array_column($this->wepay_payment_driver->payment_hash->invoices(), 'amount')) + $this->wepay_payment_driver->payment_hash->fee_total;
$app_fee = (config('ninja.wepay.fee_cc_multiplier') * $amount) + config('ninja.wepay.fee_fixed');
// charge the credit card
$response = $this->wepay_payment_driver->wepay->request('checkout/create', array(

View File

@ -68,6 +68,7 @@ class PaymentRepository extends BaseRepository {
{
$is_existing_payment = true;
$client = false;
//check currencies here and fill the exchange rate data if necessary
if (! $payment->id) {
@ -107,6 +108,10 @@ class PaymentRepository extends BaseRepository {
$payment->is_manual = true;
$payment->status_id = Payment::STATUS_COMPLETED;
if (! $payment->currency_id && $client) {
$payment->currency_id = $client->company->settings->currency_id;
}
$payment->save();
/*Save documents*/

View File

@ -65,6 +65,8 @@ class Number
$decimal = $currency->decimal_separator;
$precision = $currency->precision;
$precision = 10;
return rtrim(rtrim(number_format($value, $precision, $decimal, $thousand), "0"),$decimal);
}

View File

@ -90,6 +90,7 @@ class TemplateEngine
if (strlen($this->entity) > 1 && strlen($this->entity_id) > 1) {
$class = 'App\Models\\'.ucfirst($this->entity);
$this->entity_obj = $class::withTrashed()->where('id', $this->decodePrimaryKey($this->entity_id))->company()->first();
nlog("the entity id = ".$this->entity_obj->id);
} else {
$this->mockEntity();
}

View File

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

View File

@ -30,6 +30,7 @@
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"afosto/yaac": "^1.4",
"asm/php-ansible": "dev-main",
"authorizenet/authorizenet": "^2.0",
"bacon/bacon-qr-code": "^2.0",

181
composer.lock generated
View File

@ -4,8 +4,60 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ebb191c91f4011a448605a0fd725faff",
"content-hash": "3071902abd0fe82991f6409ab7f5b4c0",
"packages": [
{
"name": "afosto/yaac",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/afosto/yaac.git",
"reference": "ef131cfe9e6dc627968f2847a5a726e961a21cd3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/afosto/yaac/zipball/ef131cfe9e6dc627968f2847a5a726e961a21cd3",
"reference": "ef131cfe9e6dc627968f2847a5a726e961a21cd3",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-openssl": "*",
"guzzlehttp/guzzle": "^6.3|^7.0",
"league/flysystem": "^1.0|^3.0"
},
"type": "package",
"autoload": {
"psr-4": {
"Afosto\\Acme\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Afosto Team",
"homepage": "https://afosto.com"
}
],
"description": "Yet Another ACME client: a decoupled LetsEncrypt client",
"homepage": "https://afosto.com",
"keywords": [
"ACME",
"acmev2",
"afosto",
"encrypt",
"lets",
"v2"
],
"support": {
"issues": "https://github.com/afosto/yaac/issues",
"source": "https://github.com/afosto/yaac/tree/v1.4.0"
},
"time": "2022-02-18T15:18:06+00:00"
},
{
"name": "apimatic/jsonmapper",
"version": "v2.0.3",
@ -5120,71 +5172,6 @@
],
"time": "2022-01-21T13:39:10+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "6373eefe0b3274d7b702d81f2c99aa977ff97dc2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6373eefe0b3274d7b702d81f2c99aa977ff97dc2",
"reference": "6373eefe0b3274d7b702d81f2c99aa977ff97dc2",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"myclabs/php-enum": "^1.5",
"php": ">= 7.1",
"psr/http-message": "^1.0"
},
"require-dev": {
"ext-zip": "*",
"guzzlehttp/guzzle": ">= 6.3",
"mikey179/vfsstream": "^1.6",
"phpunit/phpunit": ">= 7.5"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/master"
},
"time": "2019-07-17T11:01:58+00:00"
},
{
"name": "mollie/mollie-api-php",
"version": "v2.40.1",
@ -5522,66 +5509,6 @@
},
"time": "2021-06-14T00:11:39+00:00"
},
{
"name": "myclabs/php-enum",
"version": "1.8.3",
"source": {
"type": "git",
"url": "https://github.com/myclabs/php-enum.git",
"reference": "b942d263c641ddb5190929ff840c68f78713e937"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937",
"reference": "b942d263c641ddb5190929ff840c68f78713e937",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.3 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "1.*",
"vimeo/psalm": "^4.6.2"
},
"type": "library",
"autoload": {
"psr-4": {
"MyCLabs\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP Enum contributors",
"homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
}
],
"description": "PHP Enum implementation",
"homepage": "http://github.com/myclabs/php-enum",
"keywords": [
"enum"
],
"support": {
"issues": "https://github.com/myclabs/php-enum/issues",
"source": "https://github.com/myclabs/php-enum/tree/1.8.3"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
"type": "tidelift"
}
],
"time": "2021-07-05T08:18:36+00:00"
},
{
"name": "nelexa/zip",
"version": "4.0.1",
@ -15724,5 +15651,5 @@
"platform-dev": {
"php": "^7.3|^7.4|^8.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.1.0"
}

View File

@ -0,0 +1,42 @@
<?php
use App\Models\CompanyGateway;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateStripeAppleDomainConfig extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
CompanyGateway::whereIn('gateway_key', ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23'])->cursor()->each(function($cg){
$config = $cg->getConfig();
if(!property_exists($config, 'appleDomainVerification')){
$config->appleDomainVerification = "";
$cg->setConfig($config);
$cg->save();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -216,10 +216,10 @@ class ProcessSEPA {
}
const publishableKey =
document.querySelector('meta[name="stripe-publishable-key"]') ? .content ? ?
document.querySelector('meta[name="stripe-publishable-key"]')?.content ??
'';
const stripeConnect =
document.querySelector('meta[name="stripe-account-id"]') ? .content ? ? '';
document.querySelector('meta[name="stripe-account-id"]')?.content ?? '';
new ProcessSEPA(publishableKey, stripeConnect).setupStripe().handle();

View File

@ -4545,6 +4545,9 @@ $LANG = array(
'activity_124' => ':user restored recurring expense :recurring_expense',
'fpx' => "FPX",
'to_view_entity_set_password' => 'To view the :entity you need to set password.',
'unsubscribe' => 'Unsubscribe',
'unsubscribed' => 'Unsubscribed',
'unsubscribed_text' => 'You have been removed from notifications for this document'
);
return $LANG;

File diff suppressed because it is too large Load Diff

View File

@ -177,6 +177,9 @@
</p>
@endif
</div>
@if(isset($unsubscribe_link))
<p><a href="{{$unsubscribe_link}}">{{ ctrans('texts.unsubscribe') }}</a></p>
@endif
</td>
</tr>
</table>

View File

@ -29,3 +29,6 @@
</p>
@endif
@endisset
@if(isset($unsubscribe_link))
<p><a href="{{$unsubscribe_link}}">{{ ctrans('texts.unsubscribe') }}</a></p>
@endif

View File

@ -0,0 +1,20 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.unsubscribed'))
@section('body')
<div class="flex h-screen">
<div class="m-auto md:w-1/3 lg:w-1/5">
<div class="flex flex-col items-center">
<img src="{{ $logo }}" class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
<h1 class="text-center text-3xl mt-10">{{ ctrans('texts.unsubscribed') }}</h1>
<p class="text-center opacity-75">{{ ctrans('texts.unsubscribed_text') }}</p>
</div>
</div>
</div>
@stop
@push('footer')
@endpush

View File

@ -0,0 +1,286 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.account_management'))
@section('body')
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey()}}">
<meta name="client-postal-code" content="{{ $client->postal_code }}">
<meta name="client-name" content="{{ $client->present()->name() }}">
<div class="flex flex-wrap overflow-hidden">
<div class="w-1/2 overflow-hidden">
<h1 style="font-size:24px;">Start your 14 day Pro Trial!</h1>
<p class="mt-6">Enjoy 14 days of our Pro Plan</p>
<div>
<ul class="list-decimal mx-20 w-100">
<li>Unlimited clients, invoices and quotes</li>
<li>Remove "Created by Invoice Ninja" from invoices</li>
<li>Enable emails to be sent from your GMail</li>
<li>Create subscriptions: Recurring & Auto-billing</li>
<li>API integration with 3rd party apps & platforms</li>
<li>Custom reminders</li>
<li>Attach PDF's to client emails</li>
<li>Display clients e-signature on invoices and quotes</li>
<li>Enable clients to "Approve Terms' checkbox</li>
<li>Bulk emailing of invoices and quotes</li>
</ul>
</div>
</div>
<div class="w-1/2 overflow-hidden">
<form id="card-form" action="{{ route('client.trial.response') }}" method="post">
@csrf
<input type="hidden" name="gateway_response">
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div class="form-group mb-2">
<input type="text" class="form-control block
w-full
px-3
py-2
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" id="name"
placeholder="{{ ctrans('texts.name') }}"
name="name"
value="{{$client->present()->name()}}">
</div>
<div class="form-group mb-2">
<input type="text" class="form-control block
w-full
px-3
py-2
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" id="address1"
placeholder="{{ ctrans('texts.address1') }}"
name="address1"
value="{{$client->address1}}">
</div>
<div class="form-group mb-2">
<input type="text" class="form-control block
w-full
px-3
py-2
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" id="address2"
placeholder="{{ ctrans('texts.address2') }}"
name="address2"
value="{{$client->address2}}">
</div>
<div class="flex form-group mb-2">
<div class="w-full gap-x-2 md:w-1/3">
<div class="form-group">
<input type="text" class="form-control block
w-full
px-3
py-2
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" id="city"
placeholder="{{ ctrans('texts.city') }}"
name="city"
value="{{$client->city}}">
</div>
</div>
<div class="w-full gap-x-2 md:w-1/3">
<div class="form-group">
<input type="text" class="form-control block
w-full
px-3
py-2
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" id="state"
placeholder="{{ ctrans('texts.state') }}"
name="state"
value="{{$client->state}}">
</div>
</div>
<div class="w-full gap-x-2 md:w-1/3">
<div class="form-group">
<input type="text" class="form-control block
w-full
px-3
py-2
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
rounded
transition
ease-in-out
m-0
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" id="postal_code"
placeholder="{{ ctrans('texts.postal_code') }}"
name="postal_code"
value="{{$client->postal_code}}">
</div>
</div>
</div>
<div class="form-group mb-2">
<select name="country" id="country" class="form-select w-full py-2 text-gray-700">
<option value="{{ $client->country->id}}" selected>{{ $client->country->iso_3166_2 }} ({{ $client->country->name }})</option>
@foreach($countries as $country)
<option value="{{ $country->id }}">{{ $country->iso_3166_2 }} ({{ $country->name }})></option>
@endforeach
</select>
</div>
<div class="mb-2">
<div id="card-element" class="border p-4 rounded
text-base
font-normal
text-gray-700
bg-white bg-clip-padding
border border-solid border-gray-300
focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"></div>
</div>
<div class="flex justify-end">
<button
@isset($form) form="{{ $form }}" @endisset
type="{{ $type ?? 'button' }}"
id="{{ $id ?? 'pay-now' }}"
@isset($data) @foreach($data as $prop => $value) data-{{ $prop }}="{{ $value }}" @endforeach @endisset
class="button button-primary bg-primary {{ $class ?? '' }}"
{{ isset($disabled) && $disabled === true ? 'disabled' : '' }}>
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ $slot ?? ctrans('texts.trial_call_to_action') }}</span>
</button>
</div>
<div class="flex justify-end mt-5">
<span class="text-gray-700" style="font-size:12px;">* At the end of your 14 day trial your card will be charged $10/month. Cancel anytime.</span>
</div>
</form>
</div>
@endsection
@push('footer')
<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript">
var stripe = Stripe('{{ $gateway->getPublishableKey()}}');
var client_secret = '{{ $intent->client_secret }}';
var elements = stripe.elements({
clientSecret: client_secret,
});
var cardElement = elements.create('card', {
value: {
postalCode: document.querySelector('input[name=postal_code]').content,
name: document.querySelector('input[name=name]').content
}
});
cardElement.mount('#card-element');
const form = document.getElementById('card-form');
var e = document.getElementById("country");
var country_value = e.options[e.selectedIndex].value;
document
.getElementById('pay-now')
.addEventListener('click', () => {
let payNowButton = document.getElementById('pay-now');
payNowButton = payNowButton;
payNowButton.disabled = true;
payNowButton.querySelector('svg').classList.remove('hidden');
payNowButton.querySelector('span').classList.add('hidden');
stripe.handleCardSetup(this.client_secret, cardElement, {
payment_method_data: {
billing_details: {
name: document.querySelector('input[name=name]').content,
},
}
})
.then((result) => {
if (result.error) {
let errors = document.getElementById('errors');
let payNowButton = document.getElementById('pay-now');
errors.textContent = '';
errors.textContent = result.error.message;
errors.hidden = false;
payNowButton.disabled = false;
payNowButton.querySelector('svg').classList.add('hidden');
payNowButton.querySelector('span').classList.remove('hidden');
return;
}
document.querySelector(
'input[name="gateway_response"]'
).value = JSON.stringify(result.setupIntent);
document.getElementById('card-form').submit();
});
});
</script>
@endpush

View File

@ -28,6 +28,7 @@ Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicD
Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name('client.error');
Route::get('client/payment/{contact_key}/{payment_id}', 'ClientPortal\InvitationController@paymentRouter')->middleware(['domain_db','contact_key_login']);
Route::get('client/ninja/{contact_key}/{company_key}', 'ClientPortal\NinjaPlanController@index')->name('client.ninja_contact_login')->middleware(['domain_db']);
Route::post('client/ninja/trial_confirmation', 'ClientPortal\NinjaPlanController@trial_confirmation')->name('client.trial.response')->middleware(['domain_db']);
Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_client_existence'], 'prefix' => 'client', 'as' => 'client.'], function () {
Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit
@ -112,6 +113,9 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key');
Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
Route::get('pay/{invitation_key}', 'ClientPortal\InvitationController@payInvoice')->name('pay.invoice');
Route::get('unsubscribe/{invitation_key}', 'ClientPortal\InvitationController@unsubscribe')->name('unsubscribe');
// Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe')->name('invoice.client_hash_and_invitation_key'); //should never need this
});

View File

@ -62,20 +62,87 @@ class NumberTest extends TestCase
$this->assertEquals(7.99, $converted_amount);
}
// public function testParsingFloats()
// {
// Currency::all()->each(function ($currency) {
// $amount = 123456789.12;
public function testRoundingDecimalsTwo()
{
$currency = Currency::find(1);
// $formatted_amount = Number::formatValue($amount, $currency);
$x = Number::formatValueNoTrailingZeroes(0.05, $currency);
// $float_amount = Number::parseFloat($formatted_amount);
$this->assertEquals(0.05, $x);
}
// if ($currency->precision == 0) {
// $this->assertEquals(123456789, $float_amount);
// } else {
// $this->assertEquals($amount, $float_amount);
// }
// });
// }
public function testRoundingDecimalsThree()
{
$currency = Currency::find(1);
$x = Number::formatValueNoTrailingZeroes(0.005, $currency);
$this->assertEquals(0.005, $x);
}
public function testRoundingDecimalsFour()
{
$currency = Currency::find(1);
$x = Number::formatValueNoTrailingZeroes(0.0005, $currency);
$this->assertEquals(0.0005, $x);
}
public function testRoundingDecimalsFive()
{
$currency = Currency::find(1);
$x = Number::formatValueNoTrailingZeroes(0.00005, $currency);
$this->assertEquals(0.00005, $x);
}
public function testRoundingDecimalsSix()
{
$currency = Currency::find(1);
$x = Number::formatValueNoTrailingZeroes(0.000005, $currency);
$this->assertEquals(0.000005, $x);
}
public function testRoundingDecimalsSeven()
{
$currency = Currency::find(1);
$x = Number::formatValueNoTrailingZeroes(0.0000005, $currency);
$this->assertEquals(0.0000005, $x);
}
public function testRoundingDecimalsEight()
{
$currency = Currency::find(1);
$x = Number::formatValueNoTrailingZeroes(0.00000005, $currency);
$this->assertEquals(0.00000005, $x);
}
public function testRoundingPositive()
{
$currency = Currency::find(1);
$x = Number::formatValueNoTrailingZeroes(1.5, $currency);
$this->assertEquals(1.5, $x);
$x = Number::formatValueNoTrailingZeroes(1.50, $currency);
$this->assertEquals(1.5, $x);
$x = Number::formatValueNoTrailingZeroes(1.500, $currency);
$this->assertEquals(1.5, $x);
$x = Number::formatValueNoTrailingZeroes(1.50005, $currency);
$this->assertEquals(1.50005, $x);
$x = Number::formatValueNoTrailingZeroes(1.50000005, $currency);
$this->assertEquals(1.50000005, $x);
}
}