mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-11-04 05:47:34 -05:00 
			
		
		
		
	Merge branch 'v5-develop' into v5-2003-fix-billing-subscription-integration
This commit is contained in:
		
						commit
						44bf716fc9
					
				@ -1 +1 @@
 | 
			
		||||
5.1.28
 | 
			
		||||
5.1.30
 | 
			
		||||
@ -101,6 +101,7 @@ class CreateSingleAccount extends Command
 | 
			
		||||
        $company = Company::factory()->create([
 | 
			
		||||
            'account_id' => $account->id,
 | 
			
		||||
            'slack_webhook_url' => config('ninja.notification.slack'),
 | 
			
		||||
            'default_password_timeout' => 30*60000,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $account->default_company_id = $company->id;
 | 
			
		||||
 | 
			
		||||
@ -35,12 +35,18 @@ class WebhookConfiguration
 | 
			
		||||
     */
 | 
			
		||||
    public $post_purchase_body =  '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var string
 | 
			
		||||
     */ 
 | 
			
		||||
    public $post_purchase_rest_method = 'POST';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    public static $casts = [
 | 
			
		||||
        'return_url' => 'string',
 | 
			
		||||
        'post_purchase_url' => 'string',
 | 
			
		||||
        'post_purchase_rest_method' => 'string',
 | 
			
		||||
        'post_purchase_headers' => 'array',
 | 
			
		||||
        'post_purchase_body' => 'object',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,8 @@ class CompanyFactory
 | 
			
		||||
        $company->custom_fields = (object) [];
 | 
			
		||||
        $company->subdomain = '';
 | 
			
		||||
        $company->enabled_modules = config('ninja.enabled_modules'); //32767;//8191; //4095
 | 
			
		||||
        $company->default_password_timeout = 30 * 60000;
 | 
			
		||||
        $company->default_password_timeout = 1800000;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return $company;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -350,6 +350,7 @@ class LoginController extends BaseController
 | 
			
		||||
            //     $refresh_token = $token['refresh_token'];
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            // $refresh_token = '';
 | 
			
		||||
            
 | 
			
		||||
            $name = OAuth::splitName($google->harvestName($user));
 | 
			
		||||
 | 
			
		||||
@ -359,8 +360,8 @@ class LoginController extends BaseController
 | 
			
		||||
                'password' => '',
 | 
			
		||||
                'email' => $google->harvestEmail($user),
 | 
			
		||||
                'oauth_user_id' => $google->harvestSubField($user),
 | 
			
		||||
                'oauth_user_token' => $token,
 | 
			
		||||
                'oauth_user_refresh_token' => $refresh_token,
 | 
			
		||||
                // 'oauth_user_token' => $token,
 | 
			
		||||
                // 'oauth_user_refresh_token' => $refresh_token,
 | 
			
		||||
                'oauth_provider_id' => 'google',
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,16 +15,36 @@ namespace App\Http\Controllers\ClientPortal;
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Models\BillingSubscription;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Cache;
 | 
			
		||||
use Illuminate\Support\Facades\App;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
 | 
			
		||||
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', [
 | 
			
		||||
            'billing_subscription' => $billing_subscription,
 | 
			
		||||
            '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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,9 @@ namespace App\Http\Controllers;
 | 
			
		||||
use App\Libraries\MultiDB;
 | 
			
		||||
use App\Libraries\OAuth\Providers\Google;
 | 
			
		||||
use App\Models\CompanyUser;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use App\Transformers\CompanyUserTransformer;
 | 
			
		||||
use App\Transformers\UserTransformer;
 | 
			
		||||
use Google_Client;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
 | 
			
		||||
@ -95,7 +97,46 @@ class ConnectedAccountController extends BaseController
 | 
			
		||||
            $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 = '';
 | 
			
		||||
            $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 = '';
 | 
			
		||||
 | 
			
		||||
@ -104,7 +145,6 @@ class ConnectedAccountController extends BaseController
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $connected_account = [
 | 
			
		||||
                'password' => '',
 | 
			
		||||
                'email' => $google->harvestEmail($user),
 | 
			
		||||
                'oauth_user_id' => $google->harvestSubField($user),
 | 
			
		||||
                'oauth_user_token' => $token,
 | 
			
		||||
@ -117,16 +157,14 @@ class ConnectedAccountController extends BaseController
 | 
			
		||||
            auth()->user()->email_verified_at = now();
 | 
			
		||||
            auth()->user()->save();
 | 
			
		||||
            
 | 
			
		||||
            //$ct = CompanyUser::whereUserId(auth()->user()->id);
 | 
			
		||||
            //return $this->listResponse($ct);
 | 
			
		||||
            
 | 
			
		||||
            return $this->itemResponse(auth()->user());
 | 
			
		||||
            // return $this->listResponse(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'));
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,48 +12,137 @@
 | 
			
		||||
namespace App\Http\Livewire;
 | 
			
		||||
 | 
			
		||||
use App\Factory\ClientFactory;
 | 
			
		||||
use App\Models\BillingSubscription;
 | 
			
		||||
use App\Models\ClientContact;
 | 
			
		||||
use App\Models\Invoice;
 | 
			
		||||
use App\Repositories\ClientContactRepository;
 | 
			
		||||
use App\Repositories\ClientRepository;
 | 
			
		||||
use Illuminate\Support\Facades\Auth;
 | 
			
		||||
use Illuminate\Support\Facades\Cache;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
use Livewire\Component;
 | 
			
		||||
 | 
			
		||||
class BillingPortalPurchase extends Component
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Random hash generated by backend to handle the tracking of state.
 | 
			
		||||
     *
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Password model for user input.
 | 
			
		||||
     *
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    public $password;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Instance of billing subscription.
 | 
			
		||||
     *
 | 
			
		||||
     * @var BillingSubscription
 | 
			
		||||
     */
 | 
			
		||||
    public $billing_subscription;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Instance of client contact.
 | 
			
		||||
     *
 | 
			
		||||
     * @var null|ClientContact
 | 
			
		||||
     */
 | 
			
		||||
    public $contact;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Rules for validating the form.
 | 
			
		||||
     *
 | 
			
		||||
     * @var \string[][]
 | 
			
		||||
     */
 | 
			
		||||
    protected $rules = [
 | 
			
		||||
        'email' => ['required', 'email'],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Id for CompanyGateway record.
 | 
			
		||||
     *
 | 
			
		||||
     * @var string|integer
 | 
			
		||||
     */
 | 
			
		||||
    public $company_gateway_id;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Id for GatewayType.
 | 
			
		||||
     *
 | 
			
		||||
     * @var string|integer
 | 
			
		||||
     */
 | 
			
		||||
    public $payment_method_id;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * List of steps that frontend form follows.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    public $steps = [
 | 
			
		||||
        'passed_email' => false,
 | 
			
		||||
        'existing_user' => false,
 | 
			
		||||
        'fetched_payment_methods' => false,
 | 
			
		||||
        'fetched_client' => false,
 | 
			
		||||
        'show_start_trial' => false,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * List of payment methods fetched from client.
 | 
			
		||||
     *
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    public $methods = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Instance of \App\Models\Invoice
 | 
			
		||||
     *
 | 
			
		||||
     * @var Invoice
 | 
			
		||||
     */
 | 
			
		||||
    public $invoice;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Coupon model for user input
 | 
			
		||||
     *
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    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()
 | 
			
		||||
    {
 | 
			
		||||
        $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()
 | 
			
		||||
    {
 | 
			
		||||
        $company = $this->billing_subscription->company;
 | 
			
		||||
@ -88,23 +183,47 @@ class BillingPortalPurchase extends Component
 | 
			
		||||
 | 
			
		||||
        $client_repo = new ClientRepository(new ClientContactRepository());
 | 
			
		||||
 | 
			
		||||
        $client = $client_repo->save([
 | 
			
		||||
        $data = [
 | 
			
		||||
            'name' => 'Client Name',
 | 
			
		||||
            'contacts' => [
 | 
			
		||||
                ['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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetching payment methods from the client.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ClientContact $contact
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
    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->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);
 | 
			
		||||
 | 
			
		||||
@ -113,6 +232,13 @@ class BillingPortalPurchase extends Component
 | 
			
		||||
        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)
 | 
			
		||||
    {
 | 
			
		||||
        $this->company_gateway_id = $company_gateway_id;
 | 
			
		||||
@ -121,10 +247,13 @@ class BillingPortalPurchase extends Component
 | 
			
		||||
        $this->handleBeforePaymentEvents();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method to handle events before payments.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function handleBeforePaymentEvents()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        //stubs
 | 
			
		||||
        $data = [
 | 
			
		||||
            'client_id' => $this->contact->client->id,
 | 
			
		||||
            'date' => now()->format('Y-m-d'),
 | 
			
		||||
@ -134,7 +263,7 @@ class BillingPortalPurchase extends Component
 | 
			
		||||
            ]],
 | 
			
		||||
            'user_input_promo_code' => $this->coupon,
 | 
			
		||||
            'coupon' => $this->coupon,
 | 
			
		||||
            'quantity' => 1, // Option to increase quantity
 | 
			
		||||
            'quantity' => $this->quantity,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $this->invoice = $this->billing_subscription
 | 
			
		||||
@ -148,18 +277,46 @@ class BillingPortalPurchase extends Component
 | 
			
		||||
            'billing_subscription_id' => $this->billing_subscription->id,
 | 
			
		||||
            'email' => $this->email ?? $this->contact->email,
 | 
			
		||||
            '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)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->emit('beforePaymentEventsCompleted');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    //this isn't managed here - this is taken care of in the BS
 | 
			
		||||
    public function applyCouponCode()
 | 
			
		||||
    /**
 | 
			
		||||
     * Proxy method for starting the trial.
 | 
			
		||||
     *
 | 
			
		||||
     * @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()
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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*/
 | 
			
		||||
        //$rules['name'] = 'required|min:1';
 | 
			
		||||
        $rules['settings'] = new ValidClientGroupSettingsRule();
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,6 @@ class CreditPolicy extends EntityPolicy
 | 
			
		||||
 | 
			
		||||
    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');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,6 @@ class TaskPolicy extends EntityPolicy
 | 
			
		||||
{
 | 
			
		||||
    public function create(User $user) : bool
 | 
			
		||||
    {
 | 
			
		||||
        return $user->isAdmin();
 | 
			
		||||
        return $user->isAdmin() || $user->hasPermission('create_task') || $user->hasPermission('create_all');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,17 +13,25 @@ namespace App\Services\BillingSubscription;
 | 
			
		||||
 | 
			
		||||
use App\DataMapper\InvoiceItem;
 | 
			
		||||
use App\Factory\InvoiceFactory;
 | 
			
		||||
use App\Jobs\Util\SystemLogger;
 | 
			
		||||
use App\Models\BillingSubscription;
 | 
			
		||||
use App\Models\ClientSubscription;
 | 
			
		||||
use App\Models\PaymentHash;
 | 
			
		||||
use App\Models\Product;
 | 
			
		||||
use App\Models\SystemLog;
 | 
			
		||||
use App\Repositories\InvoiceRepository;
 | 
			
		||||
use App\Utils\Traits\MakesHash;
 | 
			
		||||
use GuzzleHttp\RequestOptions;
 | 
			
		||||
 | 
			
		||||
class BillingSubscriptionService
 | 
			
		||||
{
 | 
			
		||||
    use MakesHash;
 | 
			
		||||
 | 
			
		||||
    /** @var BillingSubscription */
 | 
			
		||||
    private $billing_subscription;
 | 
			
		||||
 | 
			
		||||
    private $client_subscription;
 | 
			
		||||
 | 
			
		||||
    public function __construct(BillingSubscription $billing_subscription)
 | 
			
		||||
    {
 | 
			
		||||
        $this->billing_subscription = $billing_subscription;
 | 
			
		||||
@ -49,7 +57,11 @@ class BillingSubscriptionService
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
@ -74,6 +86,10 @@ class BillingSubscriptionService
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates the required line items for the invoice 
 | 
			
		||||
     * for the billing subscription.
 | 
			
		||||
     */
 | 
			
		||||
    private function createLineItems($data): array
 | 
			
		||||
    {
 | 
			
		||||
        $line_items = [];
 | 
			
		||||
@ -108,11 +124,14 @@ class BillingSubscriptionService
 | 
			
		||||
        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)
 | 
			
		||||
    {
 | 
			
		||||
        
 | 
			
		||||
        $product = $this->billing_subscription->product;
 | 
			
		||||
 | 
			
		||||
        $discounted_amount = 0;
 | 
			
		||||
        $discount = 0;
 | 
			
		||||
        $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.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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->subscription_id = $this->billing_subscription->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();
 | 
			
		||||
 | 
			
		||||
        $this->client_subscription = $cs;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function triggerWebhook($payment_hash)
 | 
			
		||||
    {
 | 
			
		||||
        //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()
 | 
			
		||||
 | 
			
		||||
@ -61,6 +61,7 @@ class UserTransformer extends EntityTransformer
 | 
			
		||||
            'last_confirmed_email_address' => (string) $user->last_confirmed_email_address ?: '',
 | 
			
		||||
            'google_2fa_secret' => (bool) $user->google_2fa_secret,
 | 
			
		||||
            'has_password' => (bool) $user->has_password,
 | 
			
		||||
            'oauth_user_token' => empty($user->oauth_user_token) ? '' : '***',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ trait UserNotifies
 | 
			
		||||
            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');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ return [
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    'url' => env('APP_URL', 'http://localhost'),
 | 
			
		||||
 | 
			
		||||
    'mix_url' => env('APP_URL', 'http://localhost'),
 | 
			
		||||
    /*
 | 
			
		||||
    |--------------------------------------------------------------------------
 | 
			
		||||
    | Application Timezone
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ return [
 | 
			
		||||
    'require_https' => env('REQUIRE_HTTPS', true),
 | 
			
		||||
    'app_url' => rtrim(env('APP_URL', ''), '/'),
 | 
			
		||||
    'app_domain' => env('APP_DOMAIN', ''),
 | 
			
		||||
    'app_version' => '5.1.28',
 | 
			
		||||
    'app_version' => '5.1.30',
 | 
			
		||||
    'minimum_client_version' => '5.0.16',
 | 
			
		||||
    'terms_version' => '1.0.1',
 | 
			
		||||
    'api_secret' => env('API_SECRET', false),
 | 
			
		||||
 | 
			
		||||
@ -37,6 +37,7 @@ class CompanyFactory extends Factory
 | 
			
		||||
            'db' => config('database.default'),
 | 
			
		||||
            'settings' => CompanySettings::defaults(),
 | 
			
		||||
            'is_large' => false,
 | 
			
		||||
            'default_password_timeout' => 30*60000,
 | 
			
		||||
            'enabled_modules' => config('ninja.enabled_modules'),
 | 
			
		||||
            'custom_fields' => (object) [
 | 
			
		||||
                //'invoice1' => 'Custom Date|date',
 | 
			
		||||
 | 
			
		||||
@ -37,10 +37,6 @@ class AddUniqueConstraintsOnAllEntities extends Migration
 | 
			
		||||
            $table->unique(['company_id', 'number']);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Schema::table('payment_hashes', function (Blueprint $table) {
 | 
			
		||||
            $table->unique(['hash']);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Schema::table('recurring_invoices', function (Blueprint $table) {
 | 
			
		||||
            $table->string('number')->change();
 | 
			
		||||
            $table->unique(['company_id', 'number']);
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ class AddInvoiceIdToClientSubscriptionsTable extends Migration
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('client_subscriptions', function (Blueprint $table) {
 | 
			
		||||
            $table->unsignedInteger('invoice_id')->nullable();
 | 
			
		||||
 | 
			
		||||
            $table->unsignedInteger('quantity')->default(1);
 | 
			
		||||
            $table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade')->onUpdate('cascade');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								public/css/app.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								public/css/app.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										6
									
								
								public/flutter_service_worker.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								public/flutter_service_worker.js
									
									
									
									
										vendored
									
									
								
							@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
 | 
			
		||||
const CACHE_NAME = 'flutter-app-cache';
 | 
			
		||||
const RESOURCES = {
 | 
			
		||||
  "favicon.ico": "51636d3a390451561744c42188ccd628",
 | 
			
		||||
"manifest.json": "77215c1737c7639764e64a192be2f7b8",
 | 
			
		||||
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
 | 
			
		||||
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
 | 
			
		||||
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
 | 
			
		||||
"assets/NOTICES": "e80e999afd09f0f14597c78d582d9c7c",
 | 
			
		||||
@ -27,10 +27,10 @@ const RESOURCES = {
 | 
			
		||||
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
 | 
			
		||||
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
 | 
			
		||||
"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",
 | 
			
		||||
"/": "23224b5e03519aaa87594403d54412cf",
 | 
			
		||||
"main.dart.js": "114d8affe0f4b7576170753cf9fb4c0a",
 | 
			
		||||
"main.dart.js": "23739d5559ad1f4c23c0b72dd29078f7",
 | 
			
		||||
"version.json": "b7c8971e1ab5b627fd2a4317c52b843e",
 | 
			
		||||
"favicon.png": "dca91c54388f52eded692718d5a98b8b"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										151435
									
								
								public/main.dart.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										151435
									
								
								public/main.dart.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -11,7 +11,6 @@
 | 
			
		||||
    "related_applications": [
 | 
			
		||||
        {
 | 
			
		||||
            "platform": "play",
 | 
			
		||||
            "url": "https://play.google.com/store/apps/details?id=com.invoiceninja.app",
 | 
			
		||||
            "id": "com.invoiceninja.app"
 | 
			
		||||
        }, {
 | 
			
		||||
            "platform": "itunes",
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
    "/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/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
 | 
			
		||||
    "/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
 | 
			
		||||
 | 
			
		||||
@ -4177,6 +4177,7 @@ $LANG = array(
 | 
			
		||||
     'migration_auth_label' => 'Let\'s continue by authenticating.',
 | 
			
		||||
     '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.',
 | 
			
		||||
     'billing_coupon_notice' => 'Your discount will be applied on the checkout.',
 | 
			
		||||
     'use_last_email' => 'Use last email',
 | 
			
		||||
     'activate_company' => 'Activate Company',
 | 
			
		||||
     'activate_company_help' => 'Enable emails, recurring invoices and notifications',
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
@section('meta_title', $billing_subscription->product->product_key)
 | 
			
		||||
 | 
			
		||||
@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
 | 
			
		||||
 | 
			
		||||
@push('footer')
 | 
			
		||||
 | 
			
		||||
@ -10,9 +10,35 @@
 | 
			
		||||
 | 
			
		||||
            <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())
 | 
			
		||||
                <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="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">
 | 
			
		||||
                <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'))
 | 
			
		||||
                    @component('portal.ninja2020.components.message')
 | 
			
		||||
                        {{ session('message') }}
 | 
			
		||||
                    @endcomponent
 | 
			
		||||
                @endif
 | 
			
		||||
 | 
			
		||||
                @if($this->steps['fetched_payment_methods'])
 | 
			
		||||
                @if($steps['fetched_payment_methods'])
 | 
			
		||||
                    <div class="flex items-center mt-4 text-sm">
 | 
			
		||||
                        <form action="{{ route('client.payments.process', ['hash' => $hash, 'sidebar' => 'hidden']) }}"
 | 
			
		||||
                              method="post"
 | 
			
		||||
@ -67,6 +93,17 @@
 | 
			
		||||
                            </button>
 | 
			
		||||
                        @endforeach
 | 
			
		||||
                    </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
 | 
			
		||||
                    <form wire:submit.prevent="authenticate" class="mt-8">
 | 
			
		||||
                        @csrf
 | 
			
		||||
@ -110,17 +147,12 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <form wire:submit.prevent="applyCouponCode" class="mt-4">
 | 
			
		||||
                    @csrf
 | 
			
		||||
 | 
			
		||||
                    <div class="flex items-center">
 | 
			
		||||
                        <label class="w-full mr-2">
 | 
			
		||||
                            <input type="text" wire:model.defer="coupon" class="input w-full m-0" />
 | 
			
		||||
                        </label>
 | 
			
		||||
 | 
			
		||||
                        <button class="button bg-primary m-0 text-white">{{ ctrans('texts.apply') }}</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </form>
 | 
			
		||||
                <div class="flex items-center mt-4">
 | 
			
		||||
                    <label class="w-full mr-2">
 | 
			
		||||
                        <input type="text" wire:model.lazy="coupon" class="input w-full m-0"/>
 | 
			
		||||
                        <small class="block text-gray-900 mt-2">{{ ctrans('texts.billing_coupon_notice') }}</small>
 | 
			
		||||
                    </label>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -62,7 +62,7 @@
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <h3 class="text-lg leading-6 font-medium text-gray-900">
 | 
			
		||||
                            {{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
 | 
			
		||||
                            - {{ ctrans('texts.paid') }}
 | 
			
		||||
                            - {{ \App\Models\Invoice::stringStatus($invoice->status_id) }}
 | 
			
		||||
                        </h3>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
@ -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('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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user