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

Merge pull request #5378 from turbo124/v5-stable

v5.1.41
This commit is contained in:
David Bomba 2021-04-08 21:27:09 +10:00 committed by GitHub
commit 2ef301ada8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 97114 additions and 96737 deletions

View File

@ -55,5 +55,7 @@ PHANTOMJS_PDF_GENERATION=true
PHANTOMJS_KEY='a-demo-key-with-low-quota-per-ip-address'
PHANTOMJS_SECRET=secret
UPDATE_SECRET=
COMPOSER_AUTH='{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}'
SENTRY_LARAVEL_DSN=https://cc7e8e2c678041689e53e409b7dba236@sentry.invoicing.co/5

View File

@ -1 +1 @@
5.1.40
5.1.41

View File

@ -236,6 +236,7 @@ class CompanySettings extends BaseSettings
public $id_number = ''; //@implemented
public $page_size = 'A4'; //Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6
public $page_layout = 'portrait';
public $font_size = 7; //@implemented
public $primary_font = 'Roboto';
public $secondary_font = 'Roboto';
@ -327,6 +328,7 @@ class CompanySettings extends BaseSettings
'signature_on_pdf' => 'bool',
'quote_footer' => 'string',
'page_size' => 'string',
'page_layout' => 'string',
'font_size' => 'int',
'primary_font' => 'string',
'secondary_font' => 'string',

View File

@ -0,0 +1,37 @@
<?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\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Subscriptions\ShowPlanSwitchRequest;
use App\Models\Subscription;
use Illuminate\Http\Request;
class SubscriptionPlanSwitchController extends Controller
{
/**
* Show the page for switching between plans.
*
* @param ShowPlanSwitchRequest $request
* @param Subscription $subscription
* @param string $target_subscription
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index(ShowPlanSwitchRequest $request, Subscription $subscription, Subscription $target_subscription)
{
return render('subscriptions.switch', [
'subscription' => $subscription,
'target_subscription' => $target_subscription,
]);
}
}

View File

@ -12,7 +12,6 @@
namespace App\Http\Controllers;
use \Illuminate\Support\Facades\DB;
use App\Http\Requests\Setup\CheckDatabaseRequest;
use App\Http\Requests\Setup\CheckMailRequest;
use App\Http\Requests\Setup\StoreSetupRequest;
@ -20,6 +19,7 @@ use App\Jobs\Account\CreateAccount;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Utils\CurlUtils;
use App\Utils\Ninja;
use App\Utils\SystemHealth;
use App\Utils\Traits\AppSetup;
use Beganovich\Snappdf\Snappdf;
@ -29,10 +29,12 @@ use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use \Illuminate\Support\Facades\DB;
/**
* Class SetupController.
@ -147,7 +149,9 @@ class SetupController extends Controller
DB::purge('db-ninja-01');
/* Run migrations */
Artisan::call('optimize');
if(!config('ninja.disable_auto_update'))
Artisan::call('optimize');
Artisan::call('migrate', ['--force' => true]);
Artisan::call('db:seed', ['--force' => true]);
@ -265,4 +269,29 @@ class SetupController extends Controller
return response([], 500);
}
}
public function update()
{
if( Ninja::isNinja() || !request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) )
return redirect('/');
$cacheCompiled = base_path('bootstrap/cache/compiled.php');
if (file_exists($cacheCompiled)) { unlink ($cacheCompiled); }
$cacheServices = base_path('bootstrap/cache/services.php');
if (file_exists($cacheServices)) { unlink ($cacheServices); }
Artisan::call('clear-compiled');
Artisan::call('cache:clear');
Artisan::call('debugbar:clear');
Artisan::call('route:clear');
Artisan::call('view:clear');
Artisan::call('config:clear');
Cache::flush();
Artisan::call('migrate', ['--force' => true]);
Artisan::call('db:seed', ['--force' => true]);
return redirect('/?clear_cache=true');
}
}

View File

@ -13,6 +13,7 @@
namespace App\Http\Controllers\Traits;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Hash;
@ -23,6 +24,7 @@ use Illuminate\Support\Facades\Hash;
trait VerifiesUserEmail
{
use UserSessionAttributes;
use MakesHash;
/**
* @return RedirectResponse
@ -37,14 +39,14 @@ trait VerifiesUserEmail
return $this->render('auth.confirmed', ['root' => 'themes', 'message' => ctrans('texts.wrong_confirmation')]);
}
if (is_null($user->password) || empty($user->password)) {
return $this->render('auth.confirmation_with_password', ['root' => 'themes']);
}
$user->email_verified_at = now();
$user->confirmation_code = null;
$user->save();
if (is_null($user->password) || empty($user->password) || Hash::check('', $user->password)) {
return $this->render('auth.confirmation_with_password', ['root' => 'themes', 'user_id' => $user->hashed_id]);
}
return $this->render('auth.confirmed', [
'root' => 'themes',
'message' => ctrans('texts.security_confirmation'),
@ -53,16 +55,13 @@ trait VerifiesUserEmail
public function confirmWithPassword()
{
$user = User::where('confirmation_code', request()->confirmation_code)->first();
if (! $user) {
return $this->render('auth.confirmed', ['root' => 'themes', 'message' => ctrans('texts.wrong_confirmation')]);
}
$user = User::where('id', $this->decodePrimaryKey(request()->user_id))->firstOrFail();
request()->validate([
'password' => ['required', 'min:6', 'confirmed'],
'password' => ['required', 'min:6'],
]);
$user->password = Hash::make(request()->password);
$user->email_verified_at = now();

View File

@ -109,6 +109,8 @@ class BillingPortalPurchase extends Component
'passwordless_login_sent' => false,
'started_payment' => false,
'discount_applied' => false,
'show_loading_bar' => false,
'not_eligible' => null,
];
/**
@ -269,7 +271,7 @@ class BillingPortalPurchase extends Component
$this->steps['fetched_payment_methods'] = true;
$this->methods = $contact->client->service()->getPaymentMethods(1000);
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
$this->heading_text = ctrans('texts.payment_methods');
@ -299,6 +301,7 @@ class BillingPortalPurchase extends Component
public function handleBeforePaymentEvents()
{
$this->steps['started_payment'] = true;
$this->steps['show_loading_bar'] = true;
$data = [
'client_id' => $this->contact->client->id,
@ -320,6 +323,15 @@ class BillingPortalPurchase extends Component
->fillDefaults()
->save();
$is_eligible = $this->subscription->service()->isEligible($this->contact);
if (is_array($is_eligible)) {
$this->steps['not_eligible'] = true;
$this->steps['show_loading_bar'] = false;
return;
}
Cache::put($this->hash, [
'subscription_id' => $this->subscription->id,
'email' => $this->email ?? $this->contact->email,

View File

@ -30,6 +30,7 @@ class SubscriptionInvoicesTable extends Component
->where('client_id', auth('contact')->user()->client->id)
->whereNotNull('subscription_id')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->where('balance', '=', 0)
->paginate($this->per_page);
return render('components.livewire.subscriptions-invoices-table', [

View File

@ -0,0 +1,45 @@
<?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\Http\Livewire;
use Livewire\Component;
class SubscriptionPlanSwitch extends Component
{
public $subscription;
public $target_subscription;
public $contact;
public $methods = [];
public $total;
public function mount()
{
$this->methods = $this->contact->client->service()->getPaymentMethods(100);
$this->total = 1;
}
public function handleBeforePaymentEvents()
{
// ..
}
public function render()
{
return render('components.livewire.subscription-plan-switch');
}
}

View File

@ -81,7 +81,7 @@ class StoreClientRequest extends Request
{
$input = $this->all();
//@todo implement feature permissions for > 100 clients
//@todo implement feature permissions for > 50 clients
$settings = ClientSettings::defaults();

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\ClientPortal\Subscriptions;
use App\Models\Subscription;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Http\FormRequest;
class ShowPlanSwitchRequest extends FormRequest
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return (bool)$this->subscription->allow_plan_changes;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

View File

@ -97,7 +97,9 @@ class UpdateRecurringInvoiceRequest extends Request
}
}
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
if (isset($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}
if (isset($input['auto_bill'])) {
$input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']);

View File

@ -54,6 +54,7 @@ class Account extends BaseModel
'promo_expires',
'discount_expires',
'trial_started',
'plan_expires'
];
const PLAN_FREE = 'free';

View File

@ -70,10 +70,10 @@ class Credit extends BaseModel
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
'custom_surcharge_tax1',
'custom_surcharge_tax2',
'custom_surcharge_tax3',
'custom_surcharge_tax4',
// 'custom_surcharge_tax1',
// 'custom_surcharge_tax2',
// 'custom_surcharge_tax3',
// 'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',

View File

@ -84,10 +84,10 @@ class Invoice extends BaseModel
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
'custom_surcharge_tax1',
'custom_surcharge_tax2',
'custom_surcharge_tax3',
'custom_surcharge_tax4',
// 'custom_surcharge_tax1',
// 'custom_surcharge_tax2',
// 'custom_surcharge_tax3',
// 'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',
@ -103,10 +103,6 @@ class Invoice extends BaseModel
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'custom_surcharge_tax1' => 'bool',
'custom_surcharge_tax2' => 'bool',
'custom_surcharge_tax3' => 'bool',
'custom_surcharge_tax4' => 'bool',
];
protected $with = [];

View File

@ -71,10 +71,10 @@ class Quote extends BaseModel
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
'custom_surcharge_tax1',
'custom_surcharge_tax2',
'custom_surcharge_tax3',
'custom_surcharge_tax4',
// 'custom_surcharge_tax1',
// 'custom_surcharge_tax2',
// 'custom_surcharge_tax3',
// 'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Models\RecurringInvoice;
use App\Services\Subscription\SubscriptionService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -77,4 +78,39 @@ class Subscription extends BaseModel
{
return $this->belongsTo(User::class);
}
public function nextDateByInterval($date, $frequency_id)
{
switch ($frequency_id) {
case RecurringInvoice::FREQUENCY_DAILY:
return $date->addDay();
case RecurringInvoice::FREQUENCY_WEEKLY:
return $date->addWeek();
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
return $date->addWeeks(2);
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
return $date->addWeeks(4);
case RecurringInvoice::FREQUENCY_MONTHLY:
return $date->addMonthNoOverflow();
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
return $date->addMonthsNoOverflow(2);
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
return $date->addMonthsNoOverflow(3);
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
return $date->addMonthsNoOverflow(4);
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
return $date->addMonthsNoOverflow(6);
case RecurringInvoice::FREQUENCY_ANNUALLY:
return $date->addYear();
case RecurringInvoice::FREQUENCY_TWO_YEARS:
return $date->addYears(2);
case RecurringInvoice::FREQUENCY_THREE_YEARS:
return $date->addYears(3);
default:
return null;
}
}
}

View File

@ -118,20 +118,20 @@ class SubscriptionService
/* Hits the client endpoint to determine whether the user is able to access this subscription */
public function isEligible($contact)
{
$context = [
'context' => 'is_eligible',
'subscription' => $this->subscription->hashed_id,
'contact' => $contact->hashed_id,
'contact_email' => $contact->email
'contact_email' => $contact->email,
'client' => $contact->client->hashed_id,
];
$response = $this->triggerWebhook($context);
nlog($response);
return $response;
}
/* Starts the process to create a trial
/* Starts the process to create a trial
- we create a recurring invoice, which is has its next_send_date as now() + trial_duration
- we then hit the client API end point to advise the trial payload
- we then return the user to either a predefined user endpoint, OR we return the user to the recurring invoice page.
@ -182,6 +182,11 @@ class SubscriptionService
}
public function createChangePlanInvoice($data)
{
}
public function createInvoice($data): ?\App\Models\Invoice
{
@ -229,10 +234,10 @@ class SubscriptionService
$response = false;
$body = array_merge($context, [
'company_key' => $this->subscription->company->company_key,
'company_key' => $this->subscription->company->company_key,
'account_key' => $this->subscription->company->account->key,
'db' => $this->subscription->company->db,
]);
]);
$response = $this->sendLoad($this->subscription, $body);
@ -240,10 +245,10 @@ class SubscriptionService
if(is_array($response)){
$body = $response;
}
else {
$status = $response->getStatusCode();
$response_body = $response->getBody();
$body = array_merge($body, ['status' => $status, 'response_body' => $response_body]);
@ -269,13 +274,49 @@ class SubscriptionService
//scan for any notification we are required to send
}
/**
* Get the single charge products for the
* subscription
*
* @return ?Product Collection
*/
public function products()
{
return Product::whereIn('id', $this->transformKeys(explode(",", $this->subscription->product_ids)))->get();
}
/**
* Get the recurring products for the
* subscription
*
* @return ?Product Collection
*/
public function recurring_products()
{
return Product::whereIn('id', $this->transformKeys(explode(",", $this->subscription->recurring_product_ids)))->get();
}
/**
* Get available upgrades & downgrades for the plan.
*
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public function getPlans()
{
return Subscription::query()
->where('company_id', $this->subscription->company_id)
->where('group_id', $this->subscription->group_id)
->where('id', '!=', $this->subscription->id)
->get();
}
public function completePlanChange(PaymentHash $paymentHash)
{
// .. handle redirect, after upgrade redirects, etc..
}
public function handleCancellation()
{
// ..
}
}

View File

@ -426,6 +426,9 @@ trait GeneratesCounter
}
switch ($client->company->reset_counter_frequency_id) {
case RecurringInvoice::FREQUENCY_DAILY:
$reset_date->addDay();
break;
case RecurringInvoice::FREQUENCY_WEEKLY:
$reset_date->addWeek();
break;

View File

@ -14,7 +14,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.1.40',
'app_version' => '5.1.41',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),
@ -38,7 +38,7 @@ return [
'sentry_dsn' => env('SENTRY_LARAVEL_DSN', 'https://9b4e15e575214354a7d666489783904a@sentry.invoicing.co/6'),
'environment' => env('NINJA_ENVIRONMENT', 'selfhost'), // 'hosted', 'development', 'selfhost', 'reseller'
'preconfigured_install' => env('PRECONFIGURED_INSTALL',false),
'update_secret' => env('UPDATE_SECRET', false),
// Settings used by invoiceninja.com
'terms_of_service_url' => [

View File

@ -623,7 +623,7 @@ class CreateUsersTable extends Migration
$t->unsignedInteger('vendor_id')->nullable();
$t->unsignedInteger('status_id')->index();
$t->text('number')->nullable();
$t->string('number')->nullable();
$t->float('discount')->default(0);
$t->boolean('is_amount_discount')->default(false);
@ -672,6 +672,7 @@ class CreateUsersTable extends Migration
$t->softDeletes('deleted_at', 6);
$t->index(['company_id', 'deleted_at']);
$t->unique(['company_id', 'number']);
$t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade')->onUpdate('cascade');
$t->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
$t->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');
@ -960,7 +961,7 @@ class CreateUsersTable extends Migration
$t->unsignedInteger('exchange_currency_id');
$t->index(['company_id', 'deleted_at']);
$t->unique(['company_id', 'number']);
$t->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
$t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade')->onUpdate('cascade');
$t->foreign('client_contact_id')->references('id')->on('client_contacts')->onDelete('cascade')->onUpdate('cascade');

View File

@ -25,15 +25,18 @@ class IdNumberFieldsForMissingEntities extends Migration
{
Schema::table('expenses', function (Blueprint $table) {
$table->string('number')->nullable();
$table->unique(['company_id', 'number']);
});
Schema::table('tasks', function (Blueprint $table) {
$table->string('number')->nullable();
$table->unique(['company_id', 'number']);
});
Schema::table('vendors', function (Blueprint $table) {
$table->text('vendor_hash')->nullable();
$table->text('public_notes')->nullable();
$table->unique(['company_id', 'number']);
});
Schema::table('vendor_contacts', function (Blueprint $table) {

View File

@ -24,6 +24,7 @@ class ProjectNumberColumn extends Migration
{
Schema::table('projects', function ($table) {
$table->string('number')->nullable();
$table->unique(['company_id', 'number']);
});
Schema::table('expenses', function ($t) {

View File

@ -23,10 +23,13 @@ class AddNumberFieldToClientsAndVendors extends Migration
Schema::table('clients', function (Blueprint $table) {
$table->string('id_number')->nullable();
$table->unique(['company_id', 'number']);
});
Schema::table('vendors', function (Blueprint $table) {
$table->string('id_number')->nullable();
$table->unique(['company_id', 'number']);
});
}

View File

@ -13,38 +13,7 @@ class AddUniqueConstraintsOnAllEntities extends Migration
*/
public function up()
{
Schema::table('expenses', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('tasks', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('vendors', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('payments', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('projects', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('clients', function (Blueprint $table) {
$table->unique(['company_id', 'number']);
});
Schema::table('recurring_invoices', function (Blueprint $table) {
$table->string('number')->change();
$table->unique(['company_id', 'number']);
});
Schema::table('recurring_invoice_invitations', function (Blueprint $table) {
$table->unique(['client_contact_id', 'recurring_invoice_id'],'recur_invoice_client_unique');
});
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -31,7 +31,7 @@ const RESOURCES = {
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"main.dart.js": "98adea066e120e92f7fd607882cd546d",
"main.dart.js": "ea0889d75bdaf28b60c0837c9d31e2ff",
"version.json": "e021a7a1750aa3e7d1d89b51ac9837e9"
};

64654
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

64248
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

64434
public/main.wasm.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=773c78b0cad68d2f1f2e",
"/css/app.css": "/css/app.css?id=2163e6d43930f4ad9253",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",

View File

@ -4202,6 +4202,7 @@ $LANG = array(
'invoice_task_datelog_help' => 'Add date details to the invoice line items',
'promo_code' => 'Promo code',
'recurring_invoice_issued_to' => 'Recurring invoice issued to',
'subscription' => 'Subscription',
);
return $LANG;

View File

@ -20,7 +20,6 @@
@foreach($subscription->service()->products() as $product)
<div class="flex items-center justify-between mb-4 bg-white rounded px-6 py-4 shadow-sm border">
<div>
<p class="text-sm text-xl">{{ $product->product_key }}</p>
<p class="text-sm text-gray-800">{{ $product->notes }}</p>
</div>
<div data-ref="price-and-quantity-container">
@ -124,7 +123,7 @@
@endforeach
@endif
@if($steps['started_payment'])
@if($steps['started_payment'] && $steps['show_loading_bar'])
<svg class="animate-spin h-8 w-8 text-primary" 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"
@ -209,6 +208,10 @@
<button class="button button-primary bg-primary">Apply</button>
</form>
@endif
@if($steps['not_eligible'] && !is_null($steps['not_eligible']))
<h1>{{ ctrans('texts.payment_error') }}</h1>
@endif
</div>
</div>
</div>

View File

@ -0,0 +1,31 @@
<div class="grid grid-cols-12 gap-8 mt-8">
<div class="col-span-12 md:col-span-5 md:col-start-4 px-4 py-5">
<!-- Total price -->
<div class="relative mt-8">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm leading-5">
<h1 class="text-2xl font-bold tracking-wide bg-gray-100 px-6 py-0">
{{ ctrans('texts.total') }}: {{ \App\Utils\Number::formatMoney($total, $subscription->company) }}
{{-- <small class="ml-1 line-through text-gray-500">{{ \App\Utils\Number::formatMoney($subscription->price, $subscription->company) }}</small>--}}
</h1>
</div>
</div>
<!-- Payment methods -->
<div class="mt-8 flex flex-col items-center">
<small class="block mb-4">Select a payment method:</small>
<div>
@foreach($this->methods as $method)
<button
{{-- wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')"--}}
class="px-3 py-2 border bg-white rounded mr-4 hover:border-blue-600">
{{ $method['label'] }}
</button>
@endforeach
</div>
</div>
</div>
</div>

View File

@ -19,6 +19,11 @@
<table class="min-w-full shadow rounded border border-gray-200 mt-4 credits-table">
<thead>
<tr>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<p role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.subscription') }}
</p>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<p role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.invoice') }}
@ -26,7 +31,7 @@
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
<p role="button" wire:click="sortBy('amount')" class="cursor-pointer">
{{ ctrans('texts.total') }}
{{ ctrans('texts.amount') }}
</p>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
@ -39,6 +44,9 @@
<tbody>
@forelse($invoices as $invoice)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->subscription->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}"
class="button-link text-primary">

View File

@ -19,6 +19,11 @@
<table class="min-w-full shadow rounded border border-gray-200 mt-4 credits-table">
<thead>
<tr>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<p role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.subscription') }}
</p>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<p role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.invoice') }}
@ -26,7 +31,7 @@
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
<p role="button" wire:click="sortBy('amount')" class="cursor-pointer">
{{ ctrans('texts.total') }}
{{ ctrans('texts.amount') }}
</p>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
@ -39,6 +44,9 @@
<tbody>
@forelse($recurring_invoices as $recurring_invoice)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $recurring_invoice->subscription->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
<a href="{{ route('client.recurring_invoice.show', $recurring_invoice->hashed_id) }}"
class="button-link text-primary">

View File

@ -85,5 +85,17 @@
</div>
@endif
@if($invoice->subscription->allow_plan_changes)
<div class="bg-white shadow overflow-hidden px-4 py-5 lg:rounded-lg">
<h3 class="text-lg leading-6 font-medium text-gray-900">Switch Plans:</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">Upgrade or downgrade your current plan.</p>
<div class="flex mt-4">
@foreach($invoice->subscription->service()->getPlans() as $subscription)
<a href="{{ route('client.subscription.plan_switch', ['subscription' => $invoice->subscription->hashed_id, 'target_subscription' => $subscription->hashed_id]) }}" class="border rounded px-5 py-2 hover:border-gray-800 text-sm cursor-pointer">{{ $subscription->name }}</a>
@endforeach
</div>
</div>
@endif
</div>
@endsection

View File

@ -0,0 +1,47 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.subscriptions'))
@section('body')
<div class="container mx-auto">
<!-- Top section showing details between plans -->
<div class="grid grid-cols-12 gap-8">
<!-- 1) Subscription we're switching from -->
<div
class="col-span-12 md:col-start-2 md:col-span-4 bg-white rounded px-4 py-5 shadow hover:shadow-lg">
<span class="text-sm uppercase text-gray-900">Current plan:</span>
<p class="mt-4">Placeholder text for plan notes, some text.</p>
<div class="flex justify-end mt-2">
<span>$10</span>
</div>
</div>
<div class="col-span-12 md:col-span-1 flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="hidden md:block">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="md:hidden">
<line x1="12" y1="5" x2="12" y2="19"></line>
<polyline points="19 12 12 19 5 12"></polyline>
</svg>
</div>
<!-- 2) Subscription we're switching to -->
<div class="col-span-12 md:col-span-4 bg-white rounded px-4 py-5 shadow border hover:shadow-lg group-hover:border-transparent">
<span class="text-sm uppercase text-gray-900">Switching to:</span>
<p class="mt-4">Placeholder text for plan notes, some text.</p>
<div class="flex justify-end mt-2">
<span>$10</span>
</div>
</div>
</div>
<!-- Payment box -->
@livewire('subscription-plan-switch', compact('subscription', 'target_subscription', 'contact'))
</div>
@endsection

View File

@ -11,6 +11,7 @@
<form action="{{ url()->current() }}" method="post" class="mt-6">
@csrf
<input type="hidden" name="user_id" value="{{ $user_id }}">
<div class="flex flex-col mt-4">
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
<input type="password" name="password" id="password"

View File

@ -72,6 +72,8 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
Route::get('documents/{document}/download', 'ClientPortal\DocumentController@download')->name('documents.download');
Route::resource('documents', 'ClientPortal\DocumentController')->only(['index', 'show']);
Route::get('subscriptions/{subscription}/plan_switch/{target_subscription}', 'ClientPortal\SubscriptionPlanSwitchController@index')->name('subscription.plan_switch');
Route::resource('subscriptions', 'ClientPortal\SubscriptionController')->only(['index']);
Route::post('upload', 'ClientPortal\UploadController')->name('upload.store');
@ -79,7 +81,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
});
Route::get('client/subscription/{subscription}/purchase', 'ClientPortal\SubscriptionPurchaseController@index')->name('client.subscription.purchase');
Route::get('client/subscription/{subscription}/purchase/', 'ClientPortal\SubscriptionPurchaseController@index')->name('client.subscription.purchase');
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
/*Invitation catches*/

View File

@ -8,6 +8,7 @@ Route::get('/', 'BaseController@flutterRoute')->middleware('guest');
Route::get('setup', 'SetupController@index')->middleware('guest');
Route::post('setup', 'SetupController@doSetup')->middleware('guest');
Route::get('update', 'SetupController@update')->middleware('guest');
Route::post('setup/check_db', 'SetupController@checkDB')->middleware('guest');
Route::post('setup/check_mail', 'SetupController@checkMail')->middleware('guest');

View File

@ -10,13 +10,16 @@
*/
namespace Tests\Feature\Ninja;
use App\Factory\SubscriptionFactory;
use App\Models\Account;
use App\Models\RecurringInvoice;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
use Carbon\Carbon;
/**
* @test
@ -72,4 +75,17 @@ class PlanTest extends TestCase
$this->assertEquals($filtered_plans->count(), 2);
}
public function testSubscriptionDateIncrement()
{
$subscription = SubscriptionFactory::create($this->company->id, $this->user->id);
$subscription->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$subscription->save();
$date = Carbon::parse('2020-01-01')->startOfDay();
$next_date = $subscription->nextDateByInterval($date, RecurringInvoice::FREQUENCY_MONTHLY);
$this->assertEquals($date->addMonthNoOverflow()->startOfDay(), $next_date->startOfDay());
}
}