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:
commit
f89bb1c1af
@ -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);
|
||||
|
@ -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}");
|
||||
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
];
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -374,6 +374,9 @@ class Import implements ShouldQueue
|
||||
$data['subdomain'] = MultiDB::randomSubdomainGenerator();
|
||||
|
||||
}
|
||||
else {
|
||||
$data['email_sending_method'] = 'default';
|
||||
}
|
||||
|
||||
$rules = (new UpdateCompanyRequest())->rules();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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*/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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()));
|
||||
|
@ -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
181
composer.lock
generated
@ -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"
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
4
resources/js/clients/payments/stripe-sepa.js
vendored
4
resources/js/clients/payments/stripe-sepa.js
vendored
@ -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();
|
||||
|
@ -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
@ -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>
|
||||
|
@ -29,3 +29,6 @@
|
||||
</p>
|
||||
@endif
|
||||
@endisset
|
||||
@if(isset($unsubscribe_link))
|
||||
<p><a href="{{$unsubscribe_link}}">{{ ctrans('texts.unsubscribe') }}</a></p>
|
||||
@endif
|
@ -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
|
286
resources/views/portal/ninja2020/plan/trial.blade.php
Normal file
286
resources/views/portal/ninja2020/plan/trial.blade.php
Normal 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
|
@ -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
|
||||
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user