Merge branch 'v5-develop' into v5-2003-fix-billing-subscription-integration

This commit is contained in:
Benjamin Beganović 2021-03-22 11:12:14 +01:00 committed by GitHub
commit 44bf716fc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 76209 additions and 75690 deletions

View File

@ -1 +1 @@
5.1.28 5.1.30

View File

@ -101,6 +101,7 @@ class CreateSingleAccount extends Command
$company = Company::factory()->create([ $company = Company::factory()->create([
'account_id' => $account->id, 'account_id' => $account->id,
'slack_webhook_url' => config('ninja.notification.slack'), 'slack_webhook_url' => config('ninja.notification.slack'),
'default_password_timeout' => 30*60000,
]); ]);
$account->default_company_id = $company->id; $account->default_company_id = $company->id;

View File

@ -35,12 +35,18 @@ class WebhookConfiguration
*/ */
public $post_purchase_body = ''; public $post_purchase_body = '';
/**
* @var string
*/
public $post_purchase_rest_method = 'POST';
/** /**
* @var array * @var array
*/ */
public static $casts = [ public static $casts = [
'return_url' => 'string', 'return_url' => 'string',
'post_purchase_url' => 'string', 'post_purchase_url' => 'string',
'post_purchase_rest_method' => 'string',
'post_purchase_headers' => 'array', 'post_purchase_headers' => 'array',
'post_purchase_body' => 'object', 'post_purchase_body' => 'object',
]; ];

View File

@ -35,7 +35,8 @@ class CompanyFactory
$company->custom_fields = (object) []; $company->custom_fields = (object) [];
$company->subdomain = ''; $company->subdomain = '';
$company->enabled_modules = config('ninja.enabled_modules'); //32767;//8191; //4095 $company->enabled_modules = config('ninja.enabled_modules'); //32767;//8191; //4095
$company->default_password_timeout = 30 * 60000; $company->default_password_timeout = 1800000;
return $company; return $company;
} }

View File

@ -350,6 +350,7 @@ class LoginController extends BaseController
// $refresh_token = $token['refresh_token']; // $refresh_token = $token['refresh_token'];
// } // }
// $refresh_token = '';
$name = OAuth::splitName($google->harvestName($user)); $name = OAuth::splitName($google->harvestName($user));
@ -359,8 +360,8 @@ class LoginController extends BaseController
'password' => '', 'password' => '',
'email' => $google->harvestEmail($user), 'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user), 'oauth_user_id' => $google->harvestSubField($user),
'oauth_user_token' => $token, // 'oauth_user_token' => $token,
'oauth_user_refresh_token' => $refresh_token, // 'oauth_user_refresh_token' => $refresh_token,
'oauth_provider_id' => 'google', 'oauth_provider_id' => 'google',
]; ];

View File

@ -15,16 +15,36 @@ namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\BillingSubscription; use App\Models\BillingSubscription;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class BillingSubscriptionPurchaseController extends Controller class BillingSubscriptionPurchaseController extends Controller
{ {
public function index(BillingSubscription $billing_subscription) public function index(BillingSubscription $billing_subscription, Request $request)
{ {
if ($request->has('locale')) {
$this->setLocale($request->query('locale'));
}
return view('billing-portal.purchase', [ return view('billing-portal.purchase', [
'billing_subscription' => $billing_subscription, 'billing_subscription' => $billing_subscription,
'hash' => Str::uuid()->toString(), 'hash' => Str::uuid()->toString(),
'request_data' => $request->all(),
]); ]);
} }
/**
* Set locale for incoming request.
*
* @param string $locale
*/
private function setLocale(string $locale): void
{
$record = DB::table('languages')->where('locale', $locale)->first();
if ($record) {
App::setLocale($record->locale);
}
}
} }

View File

@ -14,7 +14,9 @@ namespace App\Http\Controllers;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Libraries\OAuth\Providers\Google; use App\Libraries\OAuth\Providers\Google;
use App\Models\CompanyUser; use App\Models\CompanyUser;
use App\Models\User;
use App\Transformers\CompanyUserTransformer; use App\Transformers\CompanyUserTransformer;
use App\Transformers\UserTransformer;
use Google_Client; use Google_Client;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -95,7 +97,46 @@ class ConnectedAccountController extends BaseController
$client->setClientId(config('ninja.auth.google.client_id')); $client->setClientId(config('ninja.auth.google.client_id'));
$client->setClientSecret(config('ninja.auth.google.client_secret')); $client->setClientSecret(config('ninja.auth.google.client_secret'));
$client->setRedirectUri(config('ninja.app_url')); $client->setRedirectUri(config('ninja.app_url'));
$token = $client->authenticate(request()->input('server_auth_code')); $refresh_token = '';
$token = '';
$connected_account = [
'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id' => 'google',
'email_verified_at' =>now()
];
auth()->user()->update($connected_account);
auth()->user()->email_verified_at = now();
auth()->user()->save();
return $this->itemResponse(auth()->user());
}
return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
public function handleGmailOauth(Request $request)
{
$user = false;
$google = new Google();
$user = $google->getTokenResponse($request->input('id_token'));
if ($user) {
$client = new Google_Client();
$client->setClientId(config('ninja.auth.google.client_id'));
$client->setClientSecret(config('ninja.auth.google.client_secret'));
$client->setRedirectUri(config('ninja.app_url'));
$token = $client->authenticate($request->input('server_auth_code'));
$refresh_token = ''; $refresh_token = '';
@ -104,7 +145,6 @@ class ConnectedAccountController extends BaseController
} }
$connected_account = [ $connected_account = [
'password' => '',
'email' => $google->harvestEmail($user), 'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user), 'oauth_user_id' => $google->harvestSubField($user),
'oauth_user_token' => $token, 'oauth_user_token' => $token,
@ -116,17 +156,15 @@ class ConnectedAccountController extends BaseController
auth()->user()->update($connected_account); auth()->user()->update($connected_account);
auth()->user()->email_verified_at = now(); auth()->user()->email_verified_at = now();
auth()->user()->save(); auth()->user()->save();
//$ct = CompanyUser::whereUserId(auth()->user()->id);
//return $this->listResponse($ct);
return $this->itemResponse(auth()->user()); return $this->itemResponse(auth()->user());
// return $this->listResponse(auth()->user());
} }
return response() return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401) ->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version')) ->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version')); ->header('X-Api-Version', config('ninja.minimum_client_version'));
} }
} }

View File

@ -12,48 +12,137 @@
namespace App\Http\Livewire; namespace App\Http\Livewire;
use App\Factory\ClientFactory; use App\Factory\ClientFactory;
use App\Models\BillingSubscription;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Invoice;
use App\Repositories\ClientContactRepository; use App\Repositories\ClientContactRepository;
use App\Repositories\ClientRepository; use App\Repositories\ClientRepository;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class BillingPortalPurchase extends Component class BillingPortalPurchase extends Component
{ {
/**
* Random hash generated by backend to handle the tracking of state.
*
* @var string
*/
public $hash; public $hash;
public $heading_text = 'Log in'; /**
* Top level text on the left side of billing page.
*
* @var string
*/
public $heading_text;
/**
* E-mail address model for user input.
*
* @var string
*/
public $email; public $email;
/**
* Password model for user input.
*
* @var string
*/
public $password; public $password;
/**
* Instance of billing subscription.
*
* @var BillingSubscription
*/
public $billing_subscription; public $billing_subscription;
/**
* Instance of client contact.
*
* @var null|ClientContact
*/
public $contact; public $contact;
/**
* Rules for validating the form.
*
* @var \string[][]
*/
protected $rules = [ protected $rules = [
'email' => ['required', 'email'], 'email' => ['required', 'email'],
]; ];
/**
* Id for CompanyGateway record.
*
* @var string|integer
*/
public $company_gateway_id; public $company_gateway_id;
/**
* Id for GatewayType.
*
* @var string|integer
*/
public $payment_method_id; public $payment_method_id;
/**
* List of steps that frontend form follows.
*
* @var array
*/
public $steps = [ public $steps = [
'passed_email' => false, 'passed_email' => false,
'existing_user' => false, 'existing_user' => false,
'fetched_payment_methods' => false, 'fetched_payment_methods' => false,
'fetched_client' => false, 'fetched_client' => false,
'show_start_trial' => false,
]; ];
/**
* List of payment methods fetched from client.
*
* @var array
*/
public $methods = []; public $methods = [];
/**
* Instance of \App\Models\Invoice
*
* @var Invoice
*/
public $invoice; public $invoice;
/**
* Coupon model for user input
*
* @var string
*/
public $coupon; public $coupon;
/**
* Quantity for seats
*
* @var int
*/
public $quantity = 1;
/**
* First-hit request data (queries, locales...).
*
* @var array
*/
public $request_data;
/**
* Handle user authentication
*
* @return $this|bool|void
*/
public function authenticate() public function authenticate()
{ {
$this->validate(); $this->validate();
@ -81,6 +170,12 @@ class BillingPortalPurchase extends Component
} }
} }
/**
* Create a blank client. Used for new customers purchasing.
*
* @return mixed
* @throws \Laracasts\Presenter\Exceptions\PresenterException
*/
protected function createBlankClient() protected function createBlankClient()
{ {
$company = $this->billing_subscription->company; $company = $this->billing_subscription->company;
@ -88,23 +183,47 @@ class BillingPortalPurchase extends Component
$client_repo = new ClientRepository(new ClientContactRepository()); $client_repo = new ClientRepository(new ClientContactRepository());
$client = $client_repo->save([ $data = [
'name' => 'Client Name', 'name' => 'Client Name',
'contacts' => [ 'contacts' => [
['email' => $this->email], ['email' => $this->email],
] ],
], ClientFactory::create($company->id, $user->id)); 'settings' => [],
];
if (array_key_exists('locale', $this->request_data)) {
$record = DB::table('languages')->where('locale', $this->request_data['locale'])->first();
if ($record) {
$data['settings']['language_id'] = (string)$record->id;
}
}
$client = $client_repo->save($data, ClientFactory::create($company->id, $user->id));
return $client->contacts->first(); return $client->contacts->first();
} }
/**
* Fetching payment methods from the client.
*
* @param ClientContact $contact
* @return $this
*/
protected function getPaymentMethods(ClientContact $contact): self protected function getPaymentMethods(ClientContact $contact): self
{ {
if ($this->billing_subscription->trial_enabled) {
$this->heading_text = ctrans('texts.plan_trial');
$this->steps['show_start_trial'] = true;
return $this;
}
$this->steps['fetched_payment_methods'] = true; $this->steps['fetched_payment_methods'] = true;
$this->methods = $contact->client->service()->getPaymentMethods(1000); $this->methods = $contact->client->service()->getPaymentMethods(1000);
$this->heading_text = 'Pick a payment method'; $this->heading_text = ctrans('texts.payment_methods');
Auth::guard('contact')->login($contact); Auth::guard('contact')->login($contact);
@ -113,6 +232,13 @@ class BillingPortalPurchase extends Component
return $this; return $this;
} }
/**
* Middle method between selecting payment method &
* submitting the from to the backend.
*
* @param $company_gateway_id
* @param $gateway_type_id
*/
public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id) public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id)
{ {
$this->company_gateway_id = $company_gateway_id; $this->company_gateway_id = $company_gateway_id;
@ -121,10 +247,13 @@ class BillingPortalPurchase extends Component
$this->handleBeforePaymentEvents(); $this->handleBeforePaymentEvents();
} }
/**
* Method to handle events before payments.
*
* @return void
*/
public function handleBeforePaymentEvents() public function handleBeforePaymentEvents()
{ {
//stubs
$data = [ $data = [
'client_id' => $this->contact->client->id, 'client_id' => $this->contact->client->id,
'date' => now()->format('Y-m-d'), 'date' => now()->format('Y-m-d'),
@ -134,7 +263,7 @@ class BillingPortalPurchase extends Component
]], ]],
'user_input_promo_code' => $this->coupon, 'user_input_promo_code' => $this->coupon,
'coupon' => $this->coupon, 'coupon' => $this->coupon,
'quantity' => 1, // Option to increase quantity 'quantity' => $this->quantity,
]; ];
$this->invoice = $this->billing_subscription $this->invoice = $this->billing_subscription
@ -148,18 +277,46 @@ class BillingPortalPurchase extends Component
'billing_subscription_id' => $this->billing_subscription->id, 'billing_subscription_id' => $this->billing_subscription->id,
'email' => $this->email ?? $this->contact->email, 'email' => $this->email ?? $this->contact->email,
'client_id' => $this->contact->client->id, 'client_id' => $this->contact->client->id,
'invoice_id' => $this->invoice->id], 'invoice_id' => $this->invoice->id,
'subscription_id' => $this->billing_subscription->id],
now()->addMinutes(60) now()->addMinutes(60)
); );
$this->emit('beforePaymentEventsCompleted'); $this->emit('beforePaymentEventsCompleted');
} }
/**
//this isn't managed here - this is taken care of in the BS * Proxy method for starting the trial.
public function applyCouponCode() *
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function handleTrial()
{ {
dd('Applying coupon code: ' . $this->coupon); return $this->billing_subscription->service()->startTrial([
'email' => $this->email ?? $this->contact->email,
]);
}
/**
* Update quantity property.
*
* @param string $option
* @return int
*/
public function updateQuantity(string $option): int
{
if ($this->quantity == 1 && $option == 'decrement') {
return $this->quantity;
}
// TODO: Dave review.
if ($this->quantity >= $this->billing_subscription->max_seats_limit) {
return $this->quantity;
}
return $option == 'increment'
? $this->quantity++
: $this->quantity--;
} }
public function render() public function render()

View File

@ -47,6 +47,10 @@ class StoreClientRequest extends Request
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
} }
if (isset($this->number)) {
$rules['number'] = Rule::unique('clients')->where('company_id', auth()->user()->company()->id);
}
/* Ensure we have a client name, and that all emails are unique*/ /* Ensure we have a client name, and that all emails are unique*/
//$rules['name'] = 'required|min:1'; //$rules['name'] = 'required|min:1';
$rules['settings'] = new ValidClientGroupSettingsRule(); $rules['settings'] = new ValidClientGroupSettingsRule();

View File

@ -11,6 +11,6 @@ class CreditPolicy extends EntityPolicy
public function create(User $user) : bool public function create(User $user) : bool
{ {
return $user->isAdmin() || $user->hasPermission('create_quote') || $user->hasPermission('create_all'); return $user->isAdmin() || $user->hasPermission('create_credit') || $user->hasPermission('create_all');
} }
} }

View File

@ -20,6 +20,6 @@ class TaskPolicy extends EntityPolicy
{ {
public function create(User $user) : bool public function create(User $user) : bool
{ {
return $user->isAdmin(); return $user->isAdmin() || $user->hasPermission('create_task') || $user->hasPermission('create_all');
} }
} }

View File

@ -13,17 +13,25 @@ namespace App\Services\BillingSubscription;
use App\DataMapper\InvoiceItem; use App\DataMapper\InvoiceItem;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Jobs\Util\SystemLogger;
use App\Models\BillingSubscription; use App\Models\BillingSubscription;
use App\Models\ClientSubscription; use App\Models\ClientSubscription;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\Models\Product; use App\Models\Product;
use App\Models\SystemLog;
use App\Repositories\InvoiceRepository; use App\Repositories\InvoiceRepository;
use App\Utils\Traits\MakesHash;
use GuzzleHttp\RequestOptions;
class BillingSubscriptionService class BillingSubscriptionService
{ {
use MakesHash;
/** @var BillingSubscription */ /** @var BillingSubscription */
private $billing_subscription; private $billing_subscription;
private $client_subscription;
public function __construct(BillingSubscription $billing_subscription) public function __construct(BillingSubscription $billing_subscription)
{ {
$this->billing_subscription = $billing_subscription; $this->billing_subscription = $billing_subscription;
@ -38,18 +46,22 @@ class BillingSubscriptionService
// At this point we have some state carried from the billing page // At this point we have some state carried from the billing page
// to this, available as $payment_hash->data->billing_context. Make something awesome ⭐ // to this, available as $payment_hash->data->billing_context. Make something awesome ⭐
// create client subscription record // create client subscription record
// //
// create recurring invoice if is_recurring // create recurring invoice if is_recurring
// //
} }
public function startTrial(array $data) public function startTrial(array $data)
{ {
// Redirects from here work just fine. Livewire will respect it.
// Some magic here..
return redirect('/trial-started');
} }
public function createInvoice($data): ?\App\Models\Invoice public function createInvoice($data): ?\App\Models\Invoice
@ -74,6 +86,10 @@ class BillingSubscriptionService
} }
/**
* Creates the required line items for the invoice
* for the billing subscription.
*/
private function createLineItems($data): array private function createLineItems($data): array
{ {
$line_items = []; $line_items = [];
@ -108,11 +124,14 @@ class BillingSubscriptionService
return $line_items; return $line_items;
} }
/**
* If a coupon is entered (and is valid)
* then we apply the coupon discount with a line item.
*/
private function createPromoLine($data) private function createPromoLine($data)
{ {
$product = $this->billing_subscription->product; $product = $this->billing_subscription->product;
$discounted_amount = 0; $discounted_amount = 0;
$discount = 0; $discount = 0;
$amount = $data['quantity'] * $product->cost; $amount = $data['quantity'] * $product->cost;
@ -142,27 +161,79 @@ class BillingSubscriptionService
} }
private function convertInvoiceToRecurring() private function convertInvoiceToRecurring($payment_hash)
{ {
//The first invoice is a plain invoice - the second is fired on the recurring schedule. //The first invoice is a plain invoice - the second is fired on the recurring schedule.
} }
public function createClientSubscription($payment_hash, $recurring_invoice_id = null) public function createClientSubscription($payment_hash)
{ {
//create the client sub record //create the client subscription record
//are we in a trial phase?
//has money been paid?
//is this a recurring or one off subscription.
//?trial enabled?
$cs = new ClientSubscription(); $cs = new ClientSubscription();
$cs->subscription_id = $this->billing_subscription->id; $cs->subscription_id = $this->billing_subscription->id;
$cs->company_id = $this->billing_subscription->company_id; $cs->company_id = $this->billing_subscription->company_id;
// client_id //if is_trial
//$cs->trial_started = time();
//$cs->trial_duration = time() + duration period in seconds
//trials will not have any monies paid.
//if a payment has been made
//$cs->invoice_id = xx
//if is_recurring
//create recurring invoice from invoice
$recurring_invoice = $this->convertInvoiceToRecurring($payment_hash);
$recurring_invoice->frequency_id = $this->billing_subscription->frequency_id;
$recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d'));
//$cs->recurring_invoice_id = $recurring_invoice->id;
//?set the recurring invoice as active - set the date here also based on the frequency?
//$cs->quantity = xx
// client_id
//$cs->client_id = xx
$cs->save(); $cs->save();
$this->client_subscription = $cs;
} }
public function triggerWebhook($payment_hash) public function triggerWebhook($payment_hash)
{ {
//hit the webhook to after a successful onboarding //hit the webhook to after a successful onboarding
//$client = xxxxxxx
//todo webhook
$body = [
'billing_subscription' => $this->billing_subscription,
'client_subscription' => $this->client_subscription,
// 'client' => $client->toArray(),
];
$client = new \GuzzleHttp\Client(['headers' => $this->billing_subscription->webhook_configuration->post_purchase_headers]);
$response = $client->{$this->billing_subscription->webhook_configuration->post_purchase_rest_method}($this->billing_subscription->post_purchase_url,[
RequestOptions::JSON => ['body' => $body]
]);
SystemLogger::dispatch(
$body,
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_RESPONSE,
SystemLog::TYPE_WEBHOOK_RESPONSE,
//$client,
);
} }
public function fireNotifications() public function fireNotifications()

View File

@ -61,6 +61,7 @@ class UserTransformer extends EntityTransformer
'last_confirmed_email_address' => (string) $user->last_confirmed_email_address ?: '', 'last_confirmed_email_address' => (string) $user->last_confirmed_email_address ?: '',
'google_2fa_secret' => (bool) $user->google_2fa_secret, 'google_2fa_secret' => (bool) $user->google_2fa_secret,
'has_password' => (bool) $user->has_password, 'has_password' => (bool) $user->has_password,
'oauth_user_token' => empty($user->oauth_user_token) ? '' : '***',
]; ];
} }

View File

@ -34,7 +34,7 @@ trait UserNotifies
array_push($required_permissions, 'all_user_notifications'); array_push($required_permissions, 'all_user_notifications');
} }
if (count(array_intersect($required_permissions, $notifications->email)) >= 1 || count(array_intersect($required_permissions, ['all_user_notifications'])) >= 1 || count(array_intersect($required_permissions, 'all_notifications')) >= 1) { if (count(array_intersect($required_permissions, $notifications->email)) >= 1 || count(array_intersect($required_permissions, ['all_user_notifications'])) >= 1 || count(array_intersect($required_permissions, ['all_notifications'])) >= 1) {
array_push($notifiable_methods, 'mail'); array_push($notifiable_methods, 'mail');
} }

View File

@ -53,7 +53,7 @@ return [
*/ */
'url' => env('APP_URL', 'http://localhost'), 'url' => env('APP_URL', 'http://localhost'),
'mix_url' => env('APP_URL', 'http://localhost'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Application Timezone | Application Timezone

View File

@ -13,7 +13,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''), 'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.1.28', 'app_version' => '5.1.30',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false), 'api_secret' => env('API_SECRET', false),

View File

@ -37,6 +37,7 @@ class CompanyFactory extends Factory
'db' => config('database.default'), 'db' => config('database.default'),
'settings' => CompanySettings::defaults(), 'settings' => CompanySettings::defaults(),
'is_large' => false, 'is_large' => false,
'default_password_timeout' => 30*60000,
'enabled_modules' => config('ninja.enabled_modules'), 'enabled_modules' => config('ninja.enabled_modules'),
'custom_fields' => (object) [ 'custom_fields' => (object) [
//'invoice1' => 'Custom Date|date', //'invoice1' => 'Custom Date|date',

View File

@ -37,10 +37,6 @@ class AddUniqueConstraintsOnAllEntities extends Migration
$table->unique(['company_id', 'number']); $table->unique(['company_id', 'number']);
}); });
Schema::table('payment_hashes', function (Blueprint $table) {
$table->unique(['hash']);
});
Schema::table('recurring_invoices', function (Blueprint $table) { Schema::table('recurring_invoices', function (Blueprint $table) {
$table->string('number')->change(); $table->string('number')->change();
$table->unique(['company_id', 'number']); $table->unique(['company_id', 'number']);

View File

@ -15,7 +15,7 @@ class AddInvoiceIdToClientSubscriptionsTable extends Migration
{ {
Schema::table('client_subscriptions', function (Blueprint $table) { Schema::table('client_subscriptions', function (Blueprint $table) {
$table->unsignedInteger('invoice_id')->nullable(); $table->unsignedInteger('invoice_id')->nullable();
$table->unsignedInteger('quantity')->default(1);
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade')->onUpdate('cascade'); $table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade')->onUpdate('cascade');
}); });
} }

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache'; const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = { const RESOURCES = {
"favicon.ico": "51636d3a390451561744c42188ccd628", "favicon.ico": "51636d3a390451561744c42188ccd628",
"manifest.json": "77215c1737c7639764e64a192be2f7b8", "manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed", "icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35", "icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"assets/NOTICES": "e80e999afd09f0f14597c78d582d9c7c", "assets/NOTICES": "e80e999afd09f0f14597c78d582d9c7c",
@ -27,10 +27,10 @@ const RESOURCES = {
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3", "assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac", "assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f", "assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "3e722fd57a6db80ee119f0e2c230ccff", "assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f", "assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"/": "23224b5e03519aaa87594403d54412cf", "/": "23224b5e03519aaa87594403d54412cf",
"main.dart.js": "114d8affe0f4b7576170753cf9fb4c0a", "main.dart.js": "23739d5559ad1f4c23c0b72dd29078f7",
"version.json": "b7c8971e1ab5b627fd2a4317c52b843e", "version.json": "b7c8971e1ab5b627fd2a4317c52b843e",
"favicon.png": "dca91c54388f52eded692718d5a98b8b" "favicon.png": "dca91c54388f52eded692718d5a98b8b"
}; };

151435
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,6 @@
"related_applications": [ "related_applications": [
{ {
"platform": "play", "platform": "play",
"url": "https://play.google.com/store/apps/details?id=com.invoiceninja.app",
"id": "com.invoiceninja.app" "id": "com.invoiceninja.app"
}, { }, {
"platform": "itunes", "platform": "itunes",

View File

@ -1,6 +1,6 @@
{ {
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5", "/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=e8d6d5e8cb60bc2f15b3", "/css/app.css": "/css/app.css?id=1481aa442df903f3c38b",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4", "/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/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7", "/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",

View File

@ -4177,6 +4177,7 @@ $LANG = array(
'migration_auth_label' => 'Let\'s continue by authenticating.', 'migration_auth_label' => 'Let\'s continue by authenticating.',
'api_secret' => 'API secret', 'api_secret' => 'API secret',
'migration_api_secret_notice' => 'You can find API_SECRET in the .env file or Invoice Ninja v5. If property is missing, leave field blank.', 'migration_api_secret_notice' => 'You can find API_SECRET in the .env file or Invoice Ninja v5. If property is missing, leave field blank.',
'billing_coupon_notice' => 'Your discount will be applied on the checkout.',
'use_last_email' => 'Use last email', 'use_last_email' => 'Use last email',
'activate_company' => 'Activate Company', 'activate_company' => 'Activate Company',
'activate_company_help' => 'Enable emails, recurring invoices and notifications', 'activate_company_help' => 'Enable emails, recurring invoices and notifications',

View File

@ -2,7 +2,7 @@
@section('meta_title', $billing_subscription->product->product_key) @section('meta_title', $billing_subscription->product->product_key)
@section('body') @section('body')
@livewire('billing-portal-purchase', ['billing_subscription' => $billing_subscription, 'contact' => auth('contact')->user(), 'hash' => $hash]) @livewire('billing-portal-purchase', ['billing_subscription' => $billing_subscription, 'contact' => auth('contact')->user(), 'hash' => $hash, 'request_data' => $request_data])
@stop @stop
@push('footer') @push('footer')

View File

@ -10,9 +10,35 @@
<p class="my-6">{{ $billing_subscription->product->notes }}</p> <p class="my-6">{{ $billing_subscription->product->notes }}</p>
<span class="text-sm uppercase font-bold">{{ ctrans('texts.total') }}:</span> <span class="text-sm uppercase font-bold">{{ ctrans('texts.price') }}:</span>
<h1 class="text-2xl font-bold tracking-wide">{{ App\Utils\Number::formatMoney($billing_subscription->product->price, $billing_subscription->company) }}</h1> <div class="flex space-x-2">
<h1 class="text-2xl font-bold tracking-wide">{{ App\Utils\Number::formatMoney($billing_subscription->product->price, $billing_subscription->company) }}</h1>
@if($billing_subscription->per_seat_enabled)
<span class="text-sm">/unit</span>
@endif
</div>
<div class="flex mt-4 space-x-4 items-center">
<span class="text-sm">{{ ctrans('texts.qty') }}</span>
<button wire:click="updateQuantity('decrement')" class="bg-gray-100 border rounded p-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-minus">
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<button>{{ $quantity }}</button>
<button wire:click="updateQuantity('increment')" class="bg-gray-100 border rounded p-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-plus">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
@if(auth('contact')->user()) @if(auth('contact')->user())
<a href="{{ route('client.invoices.index') }}" class="block mt-16 inline-flex items-center space-x-2"> <a href="{{ route('client.invoices.index') }}" class="block mt-16 inline-flex items-center space-x-2">
@ -32,14 +58,14 @@
<div class="col-span-12 lg:col-span-6 bg-white lg:shadow-lg lg:h-screen"> <div class="col-span-12 lg:col-span-6 bg-white lg:shadow-lg lg:h-screen">
<div class="grid grid-cols-12 flex flex-col p-10 lg:mt-48 lg:ml-16"> <div class="grid grid-cols-12 flex flex-col p-10 lg:mt-48 lg:ml-16">
<div class="col-span-12 w-full lg:col-span-6"> <div class="col-span-12 w-full lg:col-span-6">
<h2 class="text-2xl font-bold tracking-wide">{{ $heading_text }}</h2> <h2 class="text-2xl font-bold tracking-wide">{{ $heading_text ?? ctrans('texts.login') }}</h2>
@if (session()->has('message')) @if (session()->has('message'))
@component('portal.ninja2020.components.message') @component('portal.ninja2020.components.message')
{{ session('message') }} {{ session('message') }}
@endcomponent @endcomponent
@endif @endif
@if($this->steps['fetched_payment_methods']) @if($steps['fetched_payment_methods'])
<div class="flex items-center mt-4 text-sm"> <div class="flex items-center mt-4 text-sm">
<form action="{{ route('client.payments.process', ['hash' => $hash, 'sidebar' => 'hidden']) }}" <form action="{{ route('client.payments.process', ['hash' => $hash, 'sidebar' => 'hidden']) }}"
method="post" method="post"
@ -67,6 +93,17 @@
</button> </button>
@endforeach @endforeach
</div> </div>
@elseif($steps['show_start_trial'])
<form wire:submit.prevent="handleTrial" class="mt-8">
@csrf
<p class="mb-4">Some text about the trial goes here. Details about the days, etc.</p>
<button class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
{{ ctrans('texts.trial_call_to_action') }}
</button>
</form>
@else @else
<form wire:submit.prevent="authenticate" class="mt-8"> <form wire:submit.prevent="authenticate" class="mt-8">
@csrf @csrf
@ -110,17 +147,12 @@
</div> </div>
</div> </div>
<form wire:submit.prevent="applyCouponCode" class="mt-4"> <div class="flex items-center mt-4">
@csrf <label class="w-full mr-2">
<input type="text" wire:model.lazy="coupon" class="input w-full m-0"/>
<div class="flex items-center"> <small class="block text-gray-900 mt-2">{{ ctrans('texts.billing_coupon_notice') }}</small>
<label class="w-full mr-2"> </label>
<input type="text" wire:model.defer="coupon" class="input w-full m-0" /> </div>
</label>
<button class="button bg-primary m-0 text-white">{{ ctrans('texts.apply') }}</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

@ -62,7 +62,7 @@
<div> <div>
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}} {{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.paid') }} - {{ \App\Models\Invoice::stringStatus($invoice->status_id) }}
</h3> </h3>
</div> </div>
</div> </div>

View File

@ -37,6 +37,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk'); Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk');
Route::post('connected_account', 'ConnectedAccountController@index'); Route::post('connected_account', 'ConnectedAccountController@index');
Route::post('connected_account/gmail', 'ConnectedAccountController@handleGmailOauth');
Route::resource('client_statement', 'ClientStatementController@statement'); // name = (client_statement. index / create / show / update / destroy / edit Route::resource('client_statement', 'ClientStatementController@statement'); // name = (client_statement. index / create / show / update / destroy / edit