mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 18:14:32 -04:00
commit
e889d62c4b
1
.gitignore
vendored
1
.gitignore
vendored
@ -29,3 +29,4 @@ nbproject
|
|||||||
.php_cs.cache
|
.php_cs.cache
|
||||||
public/test.pdf
|
public/test.pdf
|
||||||
public/storage/test.pdf
|
public/storage/test.pdf
|
||||||
|
/Modules
|
@ -1 +1 @@
|
|||||||
5.1.32
|
5.1.33
|
@ -52,8 +52,7 @@ class PostUpdate extends Command
|
|||||||
|
|
||||||
nlog("finished migrating");
|
nlog("finished migrating");
|
||||||
|
|
||||||
exec('vendor/bin/composer install --no-dev');
|
exec('vendor/bin/composer install --no-dev -o');
|
||||||
exec('vendor/bin/composer dump');
|
|
||||||
|
|
||||||
nlog("finished running composer install ");
|
nlog("finished running composer install ");
|
||||||
|
|
||||||
|
@ -75,6 +75,10 @@ class SendRemindersCron extends Command
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function webHookExpiredQuotes()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private function executeWebhooks()
|
private function executeWebhooks()
|
||||||
{
|
{
|
||||||
|
@ -139,6 +139,8 @@ class CompanySettings extends BaseSettings
|
|||||||
public $payment_type_id = '0'; //@TODO where do we use this?
|
public $payment_type_id = '0'; //@TODO where do we use this?
|
||||||
// public $invoice_fields = ''; //@TODO is this redundant, we store this in the custom_fields on the company?
|
// public $invoice_fields = ''; //@TODO is this redundant, we store this in the custom_fields on the company?
|
||||||
|
|
||||||
|
public $valid_until = ''; //@implemented
|
||||||
|
|
||||||
public $show_accept_invoice_terms = false; //@TODO ben to confirm
|
public $show_accept_invoice_terms = false; //@TODO ben to confirm
|
||||||
public $show_accept_quote_terms = false; //@TODO ben to confirm
|
public $show_accept_quote_terms = false; //@TODO ben to confirm
|
||||||
public $require_invoice_signature = false; //@TODO ben to confirm
|
public $require_invoice_signature = false; //@TODO ben to confirm
|
||||||
@ -430,6 +432,7 @@ class CompanySettings extends BaseSettings
|
|||||||
'show_accept_quote_terms' => 'bool',
|
'show_accept_quote_terms' => 'bool',
|
||||||
'show_accept_invoice_terms' => 'bool',
|
'show_accept_invoice_terms' => 'bool',
|
||||||
'timezone_id' => 'string',
|
'timezone_id' => 'string',
|
||||||
|
'valid_until' => 'string',
|
||||||
'date_format_id' => 'string',
|
'date_format_id' => 'string',
|
||||||
'military_time' => 'bool',
|
'military_time' => 'bool',
|
||||||
'language_id' => 'string',
|
'language_id' => 'string',
|
||||||
|
@ -51,7 +51,7 @@ class InvoiceItem
|
|||||||
|
|
||||||
public $custom_value4 = '';
|
public $custom_value4 = '';
|
||||||
|
|
||||||
public $type_id = '1'; //1 = product, 2 = service, 3 unpaid gateway fee, 4 paid gateway fee, 5 late fee, 6 promo code
|
public $type_id = '1'; //1 = product, 2 = service, 3 unpaid gateway fee, 4 paid gateway fee, 5 late fee, 6 expense
|
||||||
|
|
||||||
public static $casts = [
|
public static $casts = [
|
||||||
'type_id' => 'string',
|
'type_id' => 'string',
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace App\Events\Credit;
|
namespace App\Events\Credit;
|
||||||
|
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
|
use App\Models\CreditInvitation;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,7 +94,9 @@ class Handler extends ExceptionHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parent::report($exception);
|
if(config('ninja.expanded_logging'))
|
||||||
|
parent::report($exception);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validException($exception)
|
private function validException($exception)
|
||||||
@ -105,7 +107,7 @@ class Handler extends ExceptionHandler
|
|||||||
if (strpos($exception->getMessage(), 'Permission denied') !== false)
|
if (strpos($exception->getMessage(), 'Permission denied') !== false)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (strpos($exception->getMessage(), 'flock()') !== false)
|
if (strpos($exception->getMessage(), 'flock') !== false)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (strpos($exception->getMessage(), 'expects parameter 1 to be resource') !== false)
|
if (strpos($exception->getMessage(), 'expects parameter 1 to be resource') !== false)
|
||||||
@ -114,6 +116,8 @@ class Handler extends ExceptionHandler
|
|||||||
if (strpos($exception->getMessage(), 'fwrite()') !== false)
|
if (strpos($exception->getMessage(), 'fwrite()') !== false)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if(strpos($exception->getMessage(), 'LockableFile') !== false)
|
||||||
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -92,11 +92,13 @@ class BaseController extends Controller
|
|||||||
'company.quotes.invitations.company',
|
'company.quotes.invitations.company',
|
||||||
'company.quotes.documents',
|
'company.quotes.documents',
|
||||||
'company.tasks.documents',
|
'company.tasks.documents',
|
||||||
|
'company.subscriptions',
|
||||||
'company.tax_rates',
|
'company.tax_rates',
|
||||||
'company.tokens_hashed',
|
'company.tokens_hashed',
|
||||||
'company.vendors.contacts.company',
|
'company.vendors.contacts.company',
|
||||||
'company.vendors.documents',
|
'company.vendors.documents',
|
||||||
'company.webhooks',
|
'company.webhooks',
|
||||||
|
'company.system_logs',
|
||||||
];
|
];
|
||||||
|
|
||||||
private $mini_load = [
|
private $mini_load = [
|
||||||
@ -214,7 +216,7 @@ class BaseController extends Controller
|
|||||||
|
|
||||||
if(!$user->hasPermission('view_client'))
|
if(!$user->hasPermission('view_client'))
|
||||||
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
|
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
|
||||||
|
|
||||||
},
|
},
|
||||||
'company.company_gateways' => function ($query) use ($user) {
|
'company.company_gateways' => function ($query) use ($user) {
|
||||||
$query->whereNotNull('updated_at');
|
$query->whereNotNull('updated_at');
|
||||||
@ -339,7 +341,14 @@ class BaseController extends Controller
|
|||||||
|
|
||||||
if(!$user->isAdmin())
|
if(!$user->isAdmin())
|
||||||
$query->where('activities.user_id', $user->id);
|
$query->where('activities.user_id', $user->id);
|
||||||
|
|
||||||
|
},
|
||||||
|
'company.subscriptions'=> function ($query) use($updated_at, $user) {
|
||||||
|
$query->where('updated_at', '>=', $updated_at);
|
||||||
|
|
||||||
|
if(!$user->isAdmin())
|
||||||
|
$query->where('subscriptions.user_id', $user->id);
|
||||||
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -435,7 +444,7 @@ class BaseController extends Controller
|
|||||||
if ($this->serializer && $this->serializer != EntityTransformer::API_SERIALIZER_JSON) {
|
if ($this->serializer && $this->serializer != EntityTransformer::API_SERIALIZER_JSON) {
|
||||||
$this->entity_type = null;
|
$this->entity_type = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$resource = new Item($item, $transformer, $this->entity_type);
|
$resource = new Item($item, $transformer, $this->entity_type);
|
||||||
|
|
||||||
if (auth()->user() && request()->include_static) {
|
if (auth()->user() && request()->include_static) {
|
||||||
|
@ -26,4 +26,9 @@ class ContactHashLoginController extends Controller
|
|||||||
{
|
{
|
||||||
return redirect('/client/login');
|
return redirect('/client/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function magicLink(string $magic_link)
|
||||||
|
{
|
||||||
|
return redirect('/client/login');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,27 +61,28 @@ class RecurringInvoiceController extends Controller
|
|||||||
|
|
||||||
public function requestCancellation(Request $request, RecurringInvoice $recurring_invoice)
|
public function requestCancellation(Request $request, RecurringInvoice $recurring_invoice)
|
||||||
{
|
{
|
||||||
//todo double check the user is able to request a cancellation
|
if (is_null($recurring_invoice->subscription_id) || optional($recurring_invoice->subscription)->allow_cancellation) {
|
||||||
//can add locale specific by chaining ->locale();
|
$nmo = new NinjaMailerObject;
|
||||||
|
$nmo->mailable = (new NinjaMailer((new ClientContactRequestCancellationObject($recurring_invoice, auth()->user()))->build()));
|
||||||
$nmo = new NinjaMailerObject;
|
$nmo->company = $recurring_invoice->company;
|
||||||
$nmo->mailable = (new NinjaMailer((new ClientContactRequestCancellationObject($recurring_invoice, auth()->user()))->build()));
|
$nmo->settings = $recurring_invoice->company->settings;
|
||||||
$nmo->company = $recurring_invoice->company;
|
|
||||||
$nmo->settings = $recurring_invoice->company->settings;
|
|
||||||
|
|
||||||
$notifiable_users = $this->filterUsersByPermissions($recurring_invoice->company->company_users, $recurring_invoice, ['recurring_cancellation']);
|
$notifiable_users = $this->filterUsersByPermissions($recurring_invoice->company->company_users, $recurring_invoice, ['recurring_cancellation']);
|
||||||
|
|
||||||
$notifiable_users->each(function ($company_user) use($nmo){
|
$notifiable_users->each(function ($company_user) use($nmo){
|
||||||
|
|
||||||
$nmo->to_user = $company_user->user;
|
$nmo->to_user = $company_user->user;
|
||||||
NinjaMailerJob::dispatch($nmo);
|
NinjaMailerJob::dispatch($nmo);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//$recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user()));
|
//$recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user()));
|
||||||
|
|
||||||
return $this->render('recurring_invoices.cancellation.index', [
|
return $this->render('recurring_invoices.cancellation.index', [
|
||||||
'invoice' => $recurring_invoice,
|
'invoice' => $recurring_invoice,
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
app/Http/Controllers/ClientPortal/SubscriptionController.php
Normal file
24
app/Http/Controllers/ClientPortal/SubscriptionController.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?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 Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SubscriptionController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return render('subscriptions.index');
|
||||||
|
}
|
||||||
|
}
|
@ -392,7 +392,7 @@ class InvoiceController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($invoice->isLocked()) {
|
if ($invoice->isLocked()) {
|
||||||
return response()->json(['message' => ctrans('texts.locked_invoice')]);
|
return response()->json(['message' => ctrans('texts.locked_invoice')], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$invoice = $this->invoice_repo->save($request->all(), $invoice);
|
$invoice = $this->invoice_repo->save($request->all(), $invoice);
|
||||||
|
@ -17,13 +17,11 @@
|
|||||||
* @OA\Property(property="allow_cancellation", type="boolean", example="true", description="______"),
|
* @OA\Property(property="allow_cancellation", type="boolean", example="true", description="______"),
|
||||||
* @OA\Property(property="per_seat_enabled", type="boolean", example="true", description="______"),
|
* @OA\Property(property="per_seat_enabled", type="boolean", example="true", description="______"),
|
||||||
* @OA\Property(property="currency_id", type="integer", example="1", description="______"),
|
* @OA\Property(property="currency_id", type="integer", example="1", description="______"),
|
||||||
* @OA\Property(property="min_seats_limit", type="integer", example="1", description="______"),
|
|
||||||
* @OA\Property(property="max_seats_limit", type="integer", example="100", description="______"),
|
* @OA\Property(property="max_seats_limit", type="integer", example="100", description="______"),
|
||||||
* @OA\Property(property="trial_enabled", type="boolean", example="true", description="______"),
|
* @OA\Property(property="trial_enabled", type="boolean", example="true", description="______"),
|
||||||
* @OA\Property(property="trial_duration", type="integer", example="2", description="______"),
|
* @OA\Property(property="trial_duration", type="integer", example="2", description="______"),
|
||||||
* @OA\Property(property="allow_query_overrides", type="boolean", example="true", description="______"),
|
* @OA\Property(property="allow_query_overrides", type="boolean", example="true", description="______"),
|
||||||
* @OA\Property(property="allow_plan_changes", type="boolean", example="true", description="______"),
|
* @OA\Property(property="allow_plan_changes", type="boolean", example="true", description="______"),
|
||||||
* @OA\Property(property="plan_map", type="string", example="1", description="map describing the available upgrade/downgrade plans for this subscription"),
|
|
||||||
* @OA\Property(property="refund_period", type="integer", example="2", description="______"),
|
* @OA\Property(property="refund_period", type="integer", example="2", description="______"),
|
||||||
* @OA\Property(property="webhook_configuration", type="string", example="2", description="______"),
|
* @OA\Property(property="webhook_configuration", type="string", example="2", description="______"),
|
||||||
* @OA\Property(property="is_deleted", type="boolean", example="true", description="______"),
|
* @OA\Property(property="is_deleted", type="boolean", example="true", description="______"),
|
||||||
|
@ -66,10 +66,16 @@ class SelfUpdateController extends BaseController
|
|||||||
$repo = new GitRepository(base_path());
|
$repo = new GitRepository(base_path());
|
||||||
|
|
||||||
nlog('Are there changes to pull? '.$repo->hasChanges());
|
nlog('Are there changes to pull? '.$repo->hasChanges());
|
||||||
|
$output = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$res = $repo->pull();
|
// $res = $repo->pull();
|
||||||
|
|
||||||
|
$output = $repo->execute('pull origin');
|
||||||
|
|
||||||
} catch (GitException $e) {
|
} catch (GitException $e) {
|
||||||
|
|
||||||
|
nlog($output);
|
||||||
nlog($e->getMessage());
|
nlog($e->getMessage());
|
||||||
return response()->json(['message'=>$e->getMessage()], 500);
|
return response()->json(['message'=>$e->getMessage()], 500);
|
||||||
}
|
}
|
||||||
@ -78,7 +84,7 @@ class SelfUpdateController extends BaseController
|
|||||||
Artisan::call('ninja:post-update');
|
Artisan::call('ninja:post-update');
|
||||||
});
|
});
|
||||||
|
|
||||||
return response()->json(['message' => ''], 200);
|
return response()->json(['message' => $output], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function checkVersion()
|
public function checkVersion()
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
namespace App\Http\Livewire;
|
namespace App\Http\Livewire;
|
||||||
|
|
||||||
use App\Factory\ClientFactory;
|
use App\Factory\ClientFactory;
|
||||||
|
use App\Jobs\Mail\NinjaMailerJob;
|
||||||
|
use App\Jobs\Mail\NinjaMailerObject;
|
||||||
|
use App\Mail\ContactPasswordlessLogin;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
use App\Models\ClientContact;
|
use App\Models\ClientContact;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
@ -102,6 +105,9 @@ class BillingPortalPurchase extends Component
|
|||||||
'fetched_payment_methods' => false,
|
'fetched_payment_methods' => false,
|
||||||
'fetched_client' => false,
|
'fetched_client' => false,
|
||||||
'show_start_trial' => false,
|
'show_start_trial' => false,
|
||||||
|
'passwordless_login_sent' => false,
|
||||||
|
'started_payment' => false,
|
||||||
|
'discount_applied' => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,12 +146,27 @@ class BillingPortalPurchase extends Component
|
|||||||
public $request_data;
|
public $request_data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Price of product.
|
|
||||||
*
|
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
public $price;
|
public $price;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disabled state of passwordless login button.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $passwordless_login_btn = false;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->price = $this->subscription->price;
|
||||||
|
|
||||||
|
if (request()->query('coupon')) {
|
||||||
|
$this->coupon = request()->query('coupon');
|
||||||
|
$this->handleCoupon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle user authentication
|
* Handle user authentication
|
||||||
*
|
*
|
||||||
@ -224,6 +245,10 @@ class BillingPortalPurchase extends Component
|
|||||||
*/
|
*/
|
||||||
protected function getPaymentMethods(ClientContact $contact): self
|
protected function getPaymentMethods(ClientContact $contact): self
|
||||||
{
|
{
|
||||||
|
Auth::guard('contact')->login($contact);
|
||||||
|
|
||||||
|
$this->contact = $contact;
|
||||||
|
|
||||||
if ($this->subscription->trial_enabled) {
|
if ($this->subscription->trial_enabled) {
|
||||||
$this->heading_text = ctrans('texts.plan_trial');
|
$this->heading_text = ctrans('texts.plan_trial');
|
||||||
$this->steps['show_start_trial'] = true;
|
$this->steps['show_start_trial'] = true;
|
||||||
@ -237,10 +262,6 @@ class BillingPortalPurchase extends Component
|
|||||||
|
|
||||||
$this->heading_text = ctrans('texts.payment_methods');
|
$this->heading_text = ctrans('texts.payment_methods');
|
||||||
|
|
||||||
Auth::guard('contact')->login($contact);
|
|
||||||
|
|
||||||
$this->contact = $contact;
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,6 +287,8 @@ class BillingPortalPurchase extends Component
|
|||||||
*/
|
*/
|
||||||
public function handleBeforePaymentEvents()
|
public function handleBeforePaymentEvents()
|
||||||
{
|
{
|
||||||
|
$this->steps['started_payment'] = true;
|
||||||
|
|
||||||
$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'),
|
||||||
@ -287,13 +310,11 @@ class BillingPortalPurchase extends Component
|
|||||||
->save();
|
->save();
|
||||||
|
|
||||||
Cache::put($this->hash, [
|
Cache::put($this->hash, [
|
||||||
'subscription_id' => $this->subscription->id,
|
'subscription_id' => $this->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,
|
||||||
'quantity' => $this->quantity,
|
now()->addMinutes(60)]
|
||||||
'subscription_id' => $this->subscription->id,
|
|
||||||
now()->addMinutes(60)]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->emit('beforePaymentEventsCompleted');
|
$this->emit('beforePaymentEventsCompleted');
|
||||||
@ -340,6 +361,34 @@ class BillingPortalPurchase extends Component
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function handleCoupon()
|
||||||
|
{
|
||||||
|
if ($this->coupon == $this->subscription->promo_code) {
|
||||||
|
$this->price = $this->subscription->promo_price;
|
||||||
|
$this->steps['discount_applied'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function passwordlessLogin()
|
||||||
|
{
|
||||||
|
$this->passwordless_login_btn = true;
|
||||||
|
|
||||||
|
$contact = ClientContact::query()
|
||||||
|
->where('email', $this->email)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$mailer = new NinjaMailerObject();
|
||||||
|
$mailer->mailable = new ContactPasswordlessLogin($this->email, (string)route('client.subscription.purchase', $this->subscription->hashed_id) . '?coupon=' . $this->coupon);
|
||||||
|
$mailer->company = $this->subscription->company;
|
||||||
|
$mailer->settings = $this->subscription->company->settings;
|
||||||
|
$mailer->to_user = $contact;
|
||||||
|
|
||||||
|
NinjaMailerJob::dispatchNow($mailer);
|
||||||
|
|
||||||
|
$this->steps['passwordless_login_sent'] = true;
|
||||||
|
$this->passwordless_login_btn = false;
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
if ($this->contact instanceof ClientContact) {
|
if ($this->contact instanceof ClientContact) {
|
||||||
|
39
app/Http/Livewire/SubscriptionInvoicesTable.php
Normal file
39
app/Http/Livewire/SubscriptionInvoicesTable.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?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 App\Models\Invoice;
|
||||||
|
use App\Utils\Traits\WithSorting;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
|
class SubscriptionInvoicesTable extends Component
|
||||||
|
{
|
||||||
|
use WithPagination;
|
||||||
|
use WithSorting;
|
||||||
|
|
||||||
|
public $per_page = 10;
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$query = Invoice::query()
|
||||||
|
->where('client_id', auth('contact')->user()->client->id)
|
||||||
|
->whereNotNull('subscription_id')
|
||||||
|
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
|
||||||
|
->paginate($this->per_page);
|
||||||
|
|
||||||
|
return render('components.livewire.subscriptions-invoices-table', [
|
||||||
|
'invoices' => $query,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
39
app/Http/Livewire/SubscriptionRecurringInvoicesTable.php
Normal file
39
app/Http/Livewire/SubscriptionRecurringInvoicesTable.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?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 App\Models\RecurringInvoice;
|
||||||
|
use App\Utils\Traits\WithSorting;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
|
class SubscriptionRecurringInvoicesTable extends Component
|
||||||
|
{
|
||||||
|
use WithPagination;
|
||||||
|
use WithSorting;
|
||||||
|
|
||||||
|
public $per_page = 10;
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$query = RecurringInvoice::query()
|
||||||
|
->where('client_id', auth('contact')->user()->client->id)
|
||||||
|
->whereNotNull('subscription_id')
|
||||||
|
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
|
||||||
|
->paginate($this->per_page);
|
||||||
|
|
||||||
|
return render('components.livewire.subscriptions-recurring-invoices-table', [
|
||||||
|
'recurring_invoices' => $query,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ use App\Models\ClientContact;
|
|||||||
use Auth;
|
use Auth;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class ContactKeyLogin
|
class ContactKeyLogin
|
||||||
{
|
{
|
||||||
@ -36,31 +37,54 @@ class ContactKeyLogin
|
|||||||
if (Auth::guard('contact')->check()) {
|
if (Auth::guard('contact')->check()) {
|
||||||
Auth::guard('contact')->logout();
|
Auth::guard('contact')->logout();
|
||||||
}
|
}
|
||||||
|
nlog("merp");
|
||||||
|
nlog($request->has('magic_link'));
|
||||||
|
nlog($request->input('magic_link'));
|
||||||
|
nlog($request->all());
|
||||||
|
|
||||||
if ($request->segment(3) && config('ninja.db.multi_db_enabled')) {
|
if ($request->segment(3) && config('ninja.db.multi_db_enabled')) {
|
||||||
if (MultiDB::findAndSetDbByContactKey($request->segment(3))) {
|
if (MultiDB::findAndSetDbByContactKey($request->segment(3))) {
|
||||||
$client_contact = ClientContact::where('contact_key', $request->segment(3))->first();
|
|
||||||
Auth::guard('contact')->login($client_contact, true);
|
if($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()){
|
||||||
|
auth()->guard('contact')->login($client_contact, true);
|
||||||
return redirect()->to('client/dashboard');
|
return redirect()->to('client/dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} elseif ($request->has('contact_key')) {
|
} elseif ($request->segment(2) && $request->segment(2) == 'key_login' && $request->segment(3)) {
|
||||||
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
|
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
|
||||||
Auth::guard('contact')->login($client_contact, true);
|
auth()->guard('contact')->login($client_contact, true);
|
||||||
return redirect()->to('client/dashboard');
|
return redirect()->to('client/dashboard');
|
||||||
}
|
}
|
||||||
} elseif ($request->has('client_hash') && config('ninja.db.multi_db_enabled')) {
|
} elseif ($request->has('client_hash') && config('ninja.db.multi_db_enabled')) {
|
||||||
if (MultiDB::findAndSetDbByClientHash($request->input('client_hash'))) {
|
if (MultiDB::findAndSetDbByClientHash($request->input('client_hash'))) {
|
||||||
$client = Client::where('client_hash', $request->input('client_hash'))->first();
|
|
||||||
Auth::guard('contact')->login($client->primary_contact()->first(), true);
|
if($client = Client::where('client_hash', $request->input('client_hash'))->first()){
|
||||||
return redirect()->to('client/dashboard');
|
auth()->guard('contact')->login($client->primary_contact()->first(), true);
|
||||||
|
return redirect()->to('client/dashboard');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} elseif ($request->has('client_hash')) {
|
} elseif ($request->has('client_hash')) {
|
||||||
if ($client = Client::where('client_hash', $request->input('client_hash'))->first()) {
|
if ($client = Client::where('client_hash', $request->input('client_hash'))->first()) {
|
||||||
Auth::guard('contact')->login($client->primary_contact()->first(), true);
|
Auth::guard('contact')->login($client->primary_contact()->first(), true);
|
||||||
|
return redirect()->to('client/dashboard');
|
||||||
|
}
|
||||||
|
} elseif ($request->segment(2) && $request->segment(2) == 'magic_link' && $request->segment(3)) {
|
||||||
|
$contact_email = Cache::get($request->segment(3));
|
||||||
|
nlog("double merp");
|
||||||
|
if($client_contact = ClientContact::where('email', $contact_email)->first()){
|
||||||
|
Auth::guard('contact')->login($client_contact, true);
|
||||||
|
|
||||||
|
if ($request->query('redirect') && !empty($request->query('redirect'))) {
|
||||||
|
return redirect()->to($request->query('redirect'));
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->to('client/dashboard');
|
return redirect()->to('client/dashboard');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nlog("exit");
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\ClientPortal;
|
namespace App\Http\Requests\ClientPortal;
|
||||||
|
|
||||||
|
use App\Models\Account;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
|
use App\Utils\Ninja;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class RegisterRequest extends FormRequest
|
class RegisterRequest extends FormRequest
|
||||||
@ -43,6 +45,14 @@ class RegisterRequest extends FormRequest
|
|||||||
return Company::where('company_key', $this->company_key)->firstOrFail();
|
return Company::where('company_key', $this->company_key)->firstOrFail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->route()->parameter('company_key') && Ninja::isSelfHost()) {
|
||||||
|
$company = Account::first()->default_company;
|
||||||
|
|
||||||
|
abort_unless($company->client_can_register, 404);
|
||||||
|
|
||||||
|
return $company;
|
||||||
|
}
|
||||||
|
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,8 +67,10 @@ class UpdateCreditRequest extends Request
|
|||||||
|
|
||||||
$input = $this->decodePrimaryKeys($input);
|
$input = $this->decodePrimaryKeys($input);
|
||||||
|
|
||||||
$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']) : [];
|
||||||
|
}
|
||||||
|
|
||||||
$input['id'] = $this->credit->id;
|
$input['id'] = $this->credit->id;
|
||||||
|
|
||||||
$this->replace($input);
|
$this->replace($input);
|
||||||
|
@ -45,8 +45,6 @@ class ActionInvoiceRequest extends Request
|
|||||||
{
|
{
|
||||||
$input = $this->all();
|
$input = $this->all();
|
||||||
|
|
||||||
$this->invoice = Invoice::find($this->decodePrimary($invoice_id));
|
|
||||||
|
|
||||||
if (!array_key_exists('action', $input)) {
|
if (!array_key_exists('action', $input)) {
|
||||||
$this->error_msg = 'Action is a required field';
|
$this->error_msg = 'Action is a required field';
|
||||||
} elseif (!$this->invoiceDeletable($this->invoice)) {
|
} elseif (!$this->invoiceDeletable($this->invoice)) {
|
||||||
|
@ -66,8 +66,10 @@ class UpdateInvoiceRequest extends Request
|
|||||||
|
|
||||||
$input['id'] = $this->invoice->id;
|
$input['id'] = $this->invoice->id;
|
||||||
|
|
||||||
$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 (array_key_exists('documents', $input)) {
|
if (array_key_exists('documents', $input)) {
|
||||||
unset($input['documents']);
|
unset($input['documents']);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ use App\Http\ValidationRules\Quote\UniqueQuoteNumberRule;
|
|||||||
use App\Models\Quote;
|
use App\Models\Quote;
|
||||||
use App\Utils\Traits\CleanLineItems;
|
use App\Utils\Traits\CleanLineItems;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class StoreQuoteRequest extends Request
|
class StoreQuoteRequest extends Request
|
||||||
{
|
{
|
||||||
@ -48,7 +49,9 @@ class StoreQuoteRequest 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';
|
||||||
}
|
}
|
||||||
|
|
||||||
$rules['number'] = new UniqueQuoteNumberRule($this->all());
|
$rules['number'] = ['nullable',Rule::unique('quotes')->where('company_id', auth()->user()->company()->id)];
|
||||||
|
|
||||||
|
// $rules['number'] = new UniqueQuoteNumberRule($this->all());
|
||||||
$rules['line_items'] = 'array';
|
$rules['line_items'] = 'array';
|
||||||
|
|
||||||
return $rules;
|
return $rules;
|
||||||
|
@ -70,6 +70,10 @@ class Request extends FormRequest
|
|||||||
|
|
||||||
public function decodePrimaryKeys($input)
|
public function decodePrimaryKeys($input)
|
||||||
{
|
{
|
||||||
|
if (array_key_exists('group_id', $input) && is_string($input['group_id'])) {
|
||||||
|
$input['group_id'] = $this->decodePrimaryKey($input['group_id']);
|
||||||
|
}
|
||||||
|
|
||||||
if (array_key_exists('subscription_id', $input) && is_string($input['subscription_id'])) {
|
if (array_key_exists('subscription_id', $input) && is_string($input['subscription_id'])) {
|
||||||
$input['subscription_id'] = $this->decodePrimaryKey($input['subscription_id']);
|
$input['subscription_id'] = $this->decodePrimaryKey($input['subscription_id']);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ namespace App\Http\Requests\Subscription;
|
|||||||
|
|
||||||
use App\Http\Requests\Request;
|
use App\Http\Requests\Request;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class StoreSubscriptionRequest extends Request
|
class StoreSubscriptionRequest extends Request
|
||||||
{
|
{
|
||||||
@ -34,10 +35,8 @@ class StoreSubscriptionRequest extends Request
|
|||||||
public function rules()
|
public function rules()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'user_id' => ['sometimes'],
|
|
||||||
'product_id' => ['sometimes'],
|
'product_id' => ['sometimes'],
|
||||||
'assigned_user_id' => ['sometimes'],
|
'assigned_user_id' => ['sometimes'],
|
||||||
'company_id' => ['sometimes'],
|
|
||||||
'is_recurring' => ['sometimes'],
|
'is_recurring' => ['sometimes'],
|
||||||
'frequency_id' => ['sometimes'],
|
'frequency_id' => ['sometimes'],
|
||||||
'auto_bill' => ['sometimes'],
|
'auto_bill' => ['sometimes'],
|
||||||
@ -52,9 +51,16 @@ class StoreSubscriptionRequest extends Request
|
|||||||
'trial_duration' => ['sometimes'],
|
'trial_duration' => ['sometimes'],
|
||||||
'allow_query_overrides' => ['sometimes'],
|
'allow_query_overrides' => ['sometimes'],
|
||||||
'allow_plan_changes' => ['sometimes'],
|
'allow_plan_changes' => ['sometimes'],
|
||||||
'plan_map' => ['sometimes'],
|
|
||||||
'refund_period' => ['sometimes'],
|
'refund_period' => ['sometimes'],
|
||||||
'webhook_configuration' => ['sometimes'],
|
'webhook_configuration' => ['array'],
|
||||||
|
'name' => ['required', Rule::unique('subscriptions')->where('company_id', auth()->user()->company()->id)]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation()
|
||||||
|
{
|
||||||
|
$input = $this->all();
|
||||||
|
|
||||||
|
$this->replace($input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,7 @@ class PortalComposer
|
|||||||
$data[] = ['title' => ctrans('texts.credits'), 'url' => 'client.credits.index', 'icon' => 'credit-card'];
|
$data[] = ['title' => ctrans('texts.credits'), 'url' => 'client.credits.index', 'icon' => 'credit-card'];
|
||||||
$data[] = ['title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'shield'];
|
$data[] = ['title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'shield'];
|
||||||
$data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
|
$data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
|
||||||
|
$data[] = ['title' => ctrans('texts.subscriptions'), 'url' => 'client.subscriptions.index', 'icon' => 'calendar'];
|
||||||
|
|
||||||
if (auth()->user('contact')->client->getSetting('enable_client_portal_tasks')) {
|
if (auth()->user('contact')->client->getSetting('enable_client_portal_tasks')) {
|
||||||
$data[] = ['title' => ctrans('texts.tasks'), 'url' => 'client.dashboard', 'icon' => 'clock'];
|
$data[] = ['title' => ctrans('texts.tasks'), 'url' => 'client.dashboard', 'icon' => 'clock'];
|
||||||
|
@ -42,6 +42,7 @@ class RecurringInvoicesCron
|
|||||||
|
|
||||||
if (! config('ninja.db.multi_db_enabled')) {
|
if (! config('ninja.db.multi_db_enabled')) {
|
||||||
$recurring_invoices = RecurringInvoice::whereDate('next_send_date', '<=', now())
|
$recurring_invoices = RecurringInvoice::whereDate('next_send_date', '<=', now())
|
||||||
|
->whereNotNull('next_send_date')
|
||||||
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
|
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
|
||||||
->where('remaining_cycles', '!=', '0')
|
->where('remaining_cycles', '!=', '0')
|
||||||
->with('company')
|
->with('company')
|
||||||
@ -62,6 +63,7 @@ class RecurringInvoicesCron
|
|||||||
MultiDB::setDB($db);
|
MultiDB::setDB($db);
|
||||||
|
|
||||||
$recurring_invoices = RecurringInvoice::whereDate('next_send_date', '<=', now())
|
$recurring_invoices = RecurringInvoice::whereDate('next_send_date', '<=', now())
|
||||||
|
->whereNotNull('next_send_date')
|
||||||
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
|
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
|
||||||
->where('remaining_cycles', '!=', '0')
|
->where('remaining_cycles', '!=', '0')
|
||||||
->with('company')
|
->with('company')
|
||||||
|
@ -77,8 +77,13 @@ class EmailPayment implements ShouldQueue
|
|||||||
|
|
||||||
$email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build();
|
$email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build();
|
||||||
|
|
||||||
|
$invitation = null;
|
||||||
|
|
||||||
|
if($this->payment->invoices()->exists())
|
||||||
|
$invitation = $this->payment->invoices()->first()->invitations()->first();
|
||||||
|
|
||||||
$nmo = new NinjaMailerObject;
|
$nmo = new NinjaMailerObject;
|
||||||
$nmo->mailable = new TemplateEmail($email_builder, $this->contact);
|
$nmo->mailable = new TemplateEmail($email_builder, $this->contact, $invitation);
|
||||||
$nmo->to_user = $this->contact;
|
$nmo->to_user = $this->contact;
|
||||||
$nmo->settings = $this->settings;
|
$nmo->settings = $this->settings;
|
||||||
$nmo->company = $this->company;
|
$nmo->company = $this->company;
|
||||||
|
@ -35,6 +35,8 @@ class SendRecurring implements ShouldQueue
|
|||||||
|
|
||||||
protected $db;
|
protected $db;
|
||||||
|
|
||||||
|
public $tries = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new job instance.
|
* Create a new job instance.
|
||||||
*
|
*
|
||||||
@ -75,16 +77,6 @@ class SendRecurring implements ShouldQueue
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//Admin notification for recurring invoice sent.
|
|
||||||
if ($invoice->invitations->count() >= 1) {
|
|
||||||
$invoice->entityEmailEvent($invoice->invitations->first(), 'invoice', 'email_template_invoice');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $this->recurring_invoice->auto_bill_enabled) {
|
|
||||||
nlog("attempting to autobill {$invoice->number}");
|
|
||||||
$invoice->service()->autoBill()->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
nlog("updating recurring invoice dates");
|
nlog("updating recurring invoice dates");
|
||||||
/* Set next date here to prevent a recurring loop forming */
|
/* Set next date here to prevent a recurring loop forming */
|
||||||
$this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate()->format('Y-m-d');
|
$this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate()->format('Y-m-d');
|
||||||
@ -102,6 +94,17 @@ class SendRecurring implements ShouldQueue
|
|||||||
|
|
||||||
$this->recurring_invoice->save();
|
$this->recurring_invoice->save();
|
||||||
|
|
||||||
|
//Admin notification for recurring invoice sent.
|
||||||
|
if ($invoice->invitations->count() >= 1) {
|
||||||
|
$invoice->entityEmailEvent($invoice->invitations->first(), 'invoice', 'email_template_invoice');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $this->recurring_invoice->auto_bill_enabled) {
|
||||||
|
nlog("attempting to autobill {$invoice->number}");
|
||||||
|
$invoice->service()->autoBill()->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function failed($exception = null)
|
public function failed($exception = null)
|
||||||
|
@ -59,7 +59,7 @@ class PaymentNotification implements ShouldQueue
|
|||||||
foreach ($payment->company->company_users as $company_user) {
|
foreach ($payment->company->company_users as $company_user) {
|
||||||
$user = $company_user->user;
|
$user = $company_user->user;
|
||||||
|
|
||||||
$methods = $this->findUserEntityNotificationType($payment, $company_user, ['payment_success_all', 'all_notifications']);
|
$methods = $this->findUserEntityNotificationType($payment, $company_user, ['payment_success', 'payment_success_all', 'all_notifications']);
|
||||||
|
|
||||||
if (($key = array_search('mail', $methods)) !== false) {
|
if (($key = array_search('mail', $methods)) !== false) {
|
||||||
unset($methods[$key]);
|
unset($methods[$key]);
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace App\Mail\Admin;
|
namespace App\Mail\Admin;
|
||||||
|
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
|
use App\Utils\HtmlEngine;
|
||||||
use App\Utils\Number;
|
use App\Utils\Number;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
@ -84,6 +85,8 @@ class AutoBillingFailureObject
|
|||||||
private function getData()
|
private function getData()
|
||||||
{
|
{
|
||||||
$signature = $this->client->getSetting('email_signature');
|
$signature = $this->client->getSetting('email_signature');
|
||||||
|
$html_variables = (new HtmlEngine($this->invoices->first()->invitations->first()))->makeValues();
|
||||||
|
$signature = str_replace(array_keys($html_variables), array_values($html_variables), $signature);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'title' => ctrans(
|
'title' => ctrans(
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace App\Mail\Admin;
|
namespace App\Mail\Admin;
|
||||||
|
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
|
use App\Utils\HtmlEngine;
|
||||||
use App\Utils\Number;
|
use App\Utils\Number;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
@ -88,6 +89,8 @@ class ClientPaymentFailureObject
|
|||||||
private function getData()
|
private function getData()
|
||||||
{
|
{
|
||||||
$signature = $this->client->getSetting('email_signature');
|
$signature = $this->client->getSetting('email_signature');
|
||||||
|
$html_variables = (new HtmlEngine($this->invoices->first()->invitations->first()))->makeValues();
|
||||||
|
$signature = str_replace(array_keys($html_variables), array_values($html_variables), $signature);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'title' => ctrans(
|
'title' => ctrans(
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace App\Mail\Admin;
|
namespace App\Mail\Admin;
|
||||||
|
|
||||||
|
use App\Utils\HtmlEngine;
|
||||||
use App\Utils\Number;
|
use App\Utils\Number;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
@ -121,6 +122,10 @@ class EntityFailedSendObject
|
|||||||
private function getData()
|
private function getData()
|
||||||
{
|
{
|
||||||
$settings = $this->entity->client->getMergedSettings();
|
$settings = $this->entity->client->getMergedSettings();
|
||||||
|
$signature = $settings->email_signature;
|
||||||
|
|
||||||
|
$html_variables = (new HtmlEngine($this->invitation))->makeValues();
|
||||||
|
$signature = str_replace(array_keys($html_variables), array_values($html_variables), $signature);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'title' => $this->getSubject(),
|
'title' => $this->getSubject(),
|
||||||
@ -136,7 +141,7 @@ class EntityFailedSendObject
|
|||||||
),
|
),
|
||||||
'url' => $this->invitation->getAdminLink(),
|
'url' => $this->invitation->getAdminLink(),
|
||||||
'button' => ctrans("texts.view_{$this->entity_type}"),
|
'button' => ctrans("texts.view_{$this->entity_type}"),
|
||||||
'signature' => $settings->email_signature,
|
'signature' => $signature,
|
||||||
'logo' => $this->company->present()->logo(),
|
'logo' => $this->company->present()->logo(),
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'whitelabel' => $this->company->account->isPaid() ? true : false,
|
'whitelabel' => $this->company->account->isPaid() ? true : false,
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace App\Mail\Admin;
|
namespace App\Mail\Admin;
|
||||||
|
|
||||||
|
use App\Mail\Engine\PaymentEmailEngine;
|
||||||
use App\Utils\Number;
|
use App\Utils\Number;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
@ -62,6 +63,8 @@ class EntityPaidObject
|
|||||||
{
|
{
|
||||||
$settings = $this->payment->client->getMergedSettings();
|
$settings = $this->payment->client->getMergedSettings();
|
||||||
|
|
||||||
|
$signature = $this->generateSignature($settings);
|
||||||
|
|
||||||
$amount = Number::formatMoney($this->payment->amount, $this->payment->client);
|
$amount = Number::formatMoney($this->payment->amount, $this->payment->client);
|
||||||
|
|
||||||
$invoice_texts = ctrans('texts.invoice_number_short');
|
$invoice_texts = ctrans('texts.invoice_number_short');
|
||||||
@ -85,7 +88,6 @@ class EntityPaidObject
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
'url' => config('ninja.app_url'),
|
'url' => config('ninja.app_url'),
|
||||||
// 'url' => config('ninja.app_url') . '/payments/' . $this->payment->hashed_id, //because we have no deep linking we cannot use this
|
|
||||||
'button' => ctrans('texts.view_payment'),
|
'button' => ctrans('texts.view_payment'),
|
||||||
'signature' => $settings->email_signature,
|
'signature' => $settings->email_signature,
|
||||||
'logo' => $this->company->present()->logo(),
|
'logo' => $this->company->present()->logo(),
|
||||||
@ -95,4 +97,13 @@ class EntityPaidObject
|
|||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateSignature($settings)
|
||||||
|
{
|
||||||
|
$html_variables = (new PaymentEmailEngine($this->payment, $this->payment->client->primary_contact()->first()))->makeValues();
|
||||||
|
|
||||||
|
$signature = str_replace(array_keys($html_variables), array_values($html_variables), $settings->email_signature);
|
||||||
|
|
||||||
|
return $signature;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
54
app/Mail/ContactPasswordlessLogin.php
Normal file
54
app/Mail/ContactPasswordlessLogin.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?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\Mail;
|
||||||
|
|
||||||
|
use App\Utils\ClientPortal\MagicLink;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ContactPasswordlessLogin extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $email;
|
||||||
|
|
||||||
|
public $url = 'https://google.com';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @param string $email
|
||||||
|
* @param string $redirect
|
||||||
|
*/
|
||||||
|
public function __construct(string $email, string $redirect = '')
|
||||||
|
{
|
||||||
|
$this->email = $email;
|
||||||
|
|
||||||
|
$this->url = MagicLink::create($email, $redirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the message.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function build()
|
||||||
|
{
|
||||||
|
return $this->view('email.billing.passwordless-login');
|
||||||
|
}
|
||||||
|
}
|
@ -69,6 +69,7 @@ class BaseEmailEngine implements EngineInterface
|
|||||||
{
|
{
|
||||||
if (! empty($this->variables)) {
|
if (! empty($this->variables)) {
|
||||||
$body = str_replace(array_keys($this->variables), array_values($this->variables), $body);
|
$body = str_replace(array_keys($this->variables), array_values($this->variables), $body);
|
||||||
|
$body = str_replace(array_keys($this->variables), array_values($this->variables), $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->body = $body;
|
$this->body = $body;
|
||||||
|
@ -76,7 +76,7 @@ class PaymentEmailEngine extends BaseEmailEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function makePaymentVariables()
|
public function makePaymentVariables()
|
||||||
{
|
{
|
||||||
$data = [];
|
$data = [];
|
||||||
|
|
||||||
@ -132,9 +132,9 @@ class PaymentEmailEngine extends BaseEmailEngine
|
|||||||
$data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
|
$data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
|
||||||
$data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')];
|
$data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')];
|
||||||
|
|
||||||
$data['$contact.full_name'] = ['value' => $this->contact->present()->name(), 'label' => ctrans('texts.name')];
|
$data['$contact.full_name'] = ['value' => isset($this->contact) ? $this->contact->present()->name() : '', 'label' => ctrans('texts.name')];
|
||||||
$data['$contact.email'] = ['value' => $this->contact->email, 'label' => ctrans('texts.email')];
|
$data['$contact.email'] = ['value' => isset($this->contact) ? $this->contact->email : '', 'label' => ctrans('texts.email')];
|
||||||
$data['$contact.phone'] = ['value' => $this->contact->phone, 'label' => ctrans('texts.phone')];
|
$data['$contact.phone'] = ['value' => isset($this->contact) ? $this->contact->phone: '', 'label' => ctrans('texts.phone')];
|
||||||
|
|
||||||
$data['$contact.name'] = ['value' => isset($this->contact) ? $this->contact->present()->name() : 'no contact name on record', 'label' => ctrans('texts.contact_name')];
|
$data['$contact.name'] = ['value' => isset($this->contact) ? $this->contact->present()->name() : 'no contact name on record', 'label' => ctrans('texts.contact_name')];
|
||||||
$data['$contact.first_name'] = ['value' => isset($this->contact) ? $this->contact->first_name : '', 'label' => ctrans('texts.first_name')];
|
$data['$contact.first_name'] = ['value' => isset($this->contact) ? $this->contact->first_name : '', 'label' => ctrans('texts.first_name')];
|
||||||
|
@ -14,6 +14,7 @@ namespace App\Mail;
|
|||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\ClientContact;
|
use App\Models\ClientContact;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Utils\HtmlEngine;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Mail\Mailable;
|
use Illuminate\Mail\Mailable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
@ -52,6 +53,14 @@ class TemplateEmail extends Mailable
|
|||||||
|
|
||||||
$company = $this->client->company;
|
$company = $this->client->company;
|
||||||
|
|
||||||
|
if($this->invitation)
|
||||||
|
{
|
||||||
|
$html_variables = (new HtmlEngine($this->invitation))->makeValues();
|
||||||
|
$signature = str_replace(array_keys($html_variables), array_values($html_variables), $settings->email_signature);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$signature = $settings->email_signature;
|
||||||
|
|
||||||
$this->from(config('mail.from.address'), $this->company->present()->name());
|
$this->from(config('mail.from.address'), $this->company->present()->name());
|
||||||
|
|
||||||
if (strlen($settings->bcc_email) > 1)
|
if (strlen($settings->bcc_email) > 1)
|
||||||
@ -71,7 +80,7 @@ class TemplateEmail extends Mailable
|
|||||||
'view_link' => $this->build_email->getViewLink(),
|
'view_link' => $this->build_email->getViewLink(),
|
||||||
'view_text' => $this->build_email->getViewText(),
|
'view_text' => $this->build_email->getViewText(),
|
||||||
'title' => '',
|
'title' => '',
|
||||||
'signature' => $settings->email_signature,
|
'signature' => $signature,
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'company' => $company,
|
'company' => $company,
|
||||||
'whitelabel' => $this->client->user->account->isPaid() ? true : false,
|
'whitelabel' => $this->client->user->account->isPaid() ? true : false,
|
||||||
|
@ -391,7 +391,7 @@ class Company extends BaseModel
|
|||||||
|
|
||||||
public function system_logs()
|
public function system_logs()
|
||||||
{
|
{
|
||||||
return $this->hasMany(SystemLog::class)->orderBy('id', 'DESC')->take(50);
|
return $this->hasMany(SystemLog::class)->orderBy('id', 'DESC')->take(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function system_log_relation()
|
public function system_log_relation()
|
||||||
|
@ -97,13 +97,13 @@ class CompanyPresenter extends EntityPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSpcQrCode($client_currency, $invoice_number, $balance_due_raw)
|
public function getSpcQrCode($client_currency, $invoice_number, $balance_due_raw, $user_iban)
|
||||||
{
|
{
|
||||||
$settings = $this->entity->settings;
|
$settings = $this->entity->settings;
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
"SPC\n0200\n1\nCH860021421411198240K\nK\n{$this->name}\n{$settings->address1}\n{$settings->postal_code} {$settings->city}\n\n\nCH\n\n\n\n\n\n\n\n{$balance_due_raw}\n{$client_currency}\n\n\n\n\n\n\n\nNON\n\n{$invoice_number}\nEPD\n";
|
"SPC\n0200\n1\n{$user_iban}\nK\n{$this->name}\n{$settings->address1}\n{$settings->postal_code} {$settings->city}\n\n\nCH\n\n\n\n\n\n\n\n{$balance_due_raw}\n{$client_currency}\n\n\n\n\n\n\n\nNON\n\n{$invoice_number}\nEPD\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ class RecurringInvoice extends BaseModel
|
|||||||
{
|
{
|
||||||
return $this->morphMany(Document::class, 'documentable');
|
return $this->morphMany(Document::class, 'documentable');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatusAttribute()
|
public function getStatusAttribute()
|
||||||
{
|
{
|
||||||
if ($this->status_id == self::STATUS_ACTIVE && Carbon::parse($this->next_send_date)->isFuture()) {
|
if ($this->status_id == self::STATUS_ACTIVE && Carbon::parse($this->next_send_date)->isFuture()) {
|
||||||
@ -394,9 +394,9 @@ class RecurringInvoice extends BaseModel
|
|||||||
if ($this->remaining_cycles == -1) {
|
if ($this->remaining_cycles == -1) {
|
||||||
$iterations = 10;
|
$iterations = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
|
|
||||||
if (!Carbon::parse($this->next_send_date)) {
|
if (!Carbon::parse($this->next_send_date)) {
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
@ -407,7 +407,7 @@ class RecurringInvoice extends BaseModel
|
|||||||
// we don't add the days... we calc the day of the month!!
|
// we don't add the days... we calc the day of the month!!
|
||||||
$next_due_date = $this->calculateDueDate($next_send_date->copy()->format('Y-m-d'));
|
$next_due_date = $this->calculateDueDate($next_send_date->copy()->format('Y-m-d'));
|
||||||
$next_due_date_string = $next_due_date ? $next_due_date->format('Y-m-d') : '';
|
$next_due_date_string = $next_due_date ? $next_due_date->format('Y-m-d') : '';
|
||||||
|
|
||||||
$next_send_date = Carbon::parse($next_send_date);
|
$next_send_date = Carbon::parse($next_send_date);
|
||||||
|
|
||||||
$data[] = [
|
$data[] = [
|
||||||
@ -420,7 +420,7 @@ class RecurringInvoice extends BaseModel
|
|||||||
|
|
||||||
/*If no due date is set - unset the due_date value */
|
/*If no due date is set - unset the due_date value */
|
||||||
// if(!$this->due_date_days || $this->due_date_days == 0){
|
// if(!$this->due_date_days || $this->due_date_days == 0){
|
||||||
|
|
||||||
// foreach($data as $key => $value)
|
// foreach($data as $key => $value)
|
||||||
// $data[$key]['due_date'] = '';
|
// $data[$key]['due_date'] = '';
|
||||||
|
|
||||||
@ -468,4 +468,9 @@ class RecurringInvoice extends BaseModel
|
|||||||
{
|
{
|
||||||
return new RecurringService($this);
|
return new RecurringService($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function subscription(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Subscription::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,40 +21,39 @@ class Subscription extends BaseModel
|
|||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'assigned_user_id',
|
||||||
'product_ids',
|
'product_ids',
|
||||||
'recurring_product_ids',
|
'recurring_product_ids',
|
||||||
'company_id',
|
|
||||||
'frequency_id',
|
'frequency_id',
|
||||||
'auto_bill',
|
'auto_bill',
|
||||||
'promo_code',
|
'promo_code',
|
||||||
'promo_discount',
|
'promo_discount',
|
||||||
'is_amount_discount',
|
'is_amount_discount',
|
||||||
'allow_cancellation',
|
'allow_cancellation',
|
||||||
'per_set_enabled',
|
'per_seat_enabled',
|
||||||
'min_seats_limit',
|
|
||||||
'max_seats_limit',
|
'max_seats_limit',
|
||||||
'trial_enabled',
|
'trial_enabled',
|
||||||
'trial_duration',
|
'trial_duration',
|
||||||
'allow_query_overrides',
|
'allow_query_overrides',
|
||||||
'allow_plan_changes',
|
'allow_plan_changes',
|
||||||
'plan_map',
|
|
||||||
'refund_period',
|
'refund_period',
|
||||||
'webhook_configuration',
|
'webhook_configuration',
|
||||||
'currency_id',
|
'currency_id',
|
||||||
'group_id',
|
'group_id',
|
||||||
|
'price',
|
||||||
|
'name',
|
||||||
|
'currency_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_deleted' => 'boolean',
|
'is_deleted' => 'boolean',
|
||||||
'plan_map' => 'object',
|
'webhook_configuration' => 'array',
|
||||||
'webhook_configuration' => 'object',
|
|
||||||
'updated_at' => 'timestamp',
|
'updated_at' => 'timestamp',
|
||||||
'created_at' => 'timestamp',
|
'created_at' => 'timestamp',
|
||||||
'deleted_at' => 'timestamp',
|
'deleted_at' => 'timestamp',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function service()
|
public function service(): SubscriptionService
|
||||||
{
|
{
|
||||||
return new SubscriptionService($this);
|
return new SubscriptionService($this);
|
||||||
}
|
}
|
||||||
@ -68,9 +67,4 @@ class Subscription extends BaseModel
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function product(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Product::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -242,7 +242,7 @@ class BaseDriver extends AbstractPaymentDriver
|
|||||||
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
|
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
|
||||||
|
|
||||||
if (property_exists($this->payment_hash->data, 'billing_context')) {
|
if (property_exists($this->payment_hash->data, 'billing_context')) {
|
||||||
$billing_subscription = \App\Models\Subscription::find($this->payment_hash->data->billing_context->billing_subscription_id);
|
$billing_subscription = \App\Models\Subscription::find($this->payment_hash->data->billing_context->subscription_id);
|
||||||
|
|
||||||
(new SubscriptionService($billing_subscription))->completePurchase($this->payment_hash);
|
(new SubscriptionService($billing_subscription))->completePurchase($this->payment_hash);
|
||||||
}
|
}
|
||||||
|
@ -175,7 +175,6 @@ class StripePaymentDriver extends BaseDriver
|
|||||||
$fields[] = ['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'required'];
|
$fields[] = ['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'required'];
|
||||||
$fields[] = ['name' => 'client_city', 'label' => ctrans('texts.city'), 'type' => 'text', 'validation' => 'required'];
|
$fields[] = ['name' => 'client_city', 'label' => ctrans('texts.city'), 'type' => 'text', 'validation' => 'required'];
|
||||||
$fields[] = ['name' => 'client_state', 'label' => ctrans('texts.state'), 'type' => 'text', 'validation' => 'required'];
|
$fields[] = ['name' => 'client_state', 'label' => ctrans('texts.state'), 'type' => 'text', 'validation' => 'required'];
|
||||||
$fields[] = ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'];
|
|
||||||
$fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required'];
|
$fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,7 +182,6 @@ class PaymentRepository extends BaseRepository {
|
|||||||
$company_currency = $client->company->settings->currency_id;
|
$company_currency = $client->company->settings->currency_id;
|
||||||
|
|
||||||
if ($company_currency != $client_currency) {
|
if ($company_currency != $client_currency) {
|
||||||
$currency = $client->currency();
|
|
||||||
|
|
||||||
$exchange_rate = new CurrencyApi();
|
$exchange_rate = new CurrencyApi();
|
||||||
|
|
||||||
|
@ -24,18 +24,6 @@ class RecurringInvoiceRepository extends BaseRepository
|
|||||||
{
|
{
|
||||||
|
|
||||||
$invoice = $this->alternativeSave($data, $invoice);
|
$invoice = $this->alternativeSave($data, $invoice);
|
||||||
// $invoice->fill($data);
|
|
||||||
|
|
||||||
// $invoice->save();
|
|
||||||
|
|
||||||
// $invoice_calc = new InvoiceSum($invoice);
|
|
||||||
|
|
||||||
// $invoice->service()
|
|
||||||
// ->applyNumber()
|
|
||||||
// ->createInvitations()
|
|
||||||
// ->save();
|
|
||||||
|
|
||||||
// $invoice = $invoice_calc->build()->getRecurringInvoice();
|
|
||||||
|
|
||||||
return $invoice;
|
return $invoice;
|
||||||
}
|
}
|
||||||
|
@ -13,16 +13,128 @@
|
|||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
|
||||||
|
use App\DataMapper\InvoiceItem;
|
||||||
|
use App\Factory\InvoiceFactory;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\InvoiceInvitation;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
|
use App\Utils\Traits\CleanLineItems;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class SubscriptionRepository extends BaseRepository
|
class SubscriptionRepository extends BaseRepository
|
||||||
{
|
{
|
||||||
|
use CleanLineItems;
|
||||||
|
|
||||||
public function save($data, Subscription $subscription): ?Subscription
|
public function save($data, Subscription $subscription): ?Subscription
|
||||||
{
|
{
|
||||||
$subscription
|
$subscription->fill($data);
|
||||||
->fill($data)
|
|
||||||
->save();
|
$calculated_prices = $this->calculatePrice($subscription);
|
||||||
|
|
||||||
|
$subscription->price = $calculated_prices['price'];
|
||||||
|
$subscription->promo_price = $calculated_prices['promo_price'];
|
||||||
|
|
||||||
|
$subscription->save();
|
||||||
|
|
||||||
return $subscription;
|
return $subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function calculatePrice($subscription) :array
|
||||||
|
{
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$client = Client::factory()->create([
|
||||||
|
'user_id' => $subscription->user_id,
|
||||||
|
'company_id' => $subscription->company_id,
|
||||||
|
'group_settings_id' => $subscription->group_id,
|
||||||
|
'country_id' => $subscription->company->settings->country_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contact = ClientContact::factory()->create([
|
||||||
|
'user_id' => $subscription->user_id,
|
||||||
|
'company_id' => $subscription->company_id,
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'is_primary' => 1,
|
||||||
|
'send_email' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$invoice = InvoiceFactory::create($subscription->company_id, $subscription->user_id);
|
||||||
|
$invoice->client_id = $client->id;
|
||||||
|
|
||||||
|
$invoice->save();
|
||||||
|
|
||||||
|
$invitation = InvoiceInvitation::factory()->create([
|
||||||
|
'user_id' => $subscription->user_id,
|
||||||
|
'company_id' => $subscription->company_id,
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'client_contact_id' => $contact->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$invoice->setRelation('invitations', $invitation);
|
||||||
|
$invoice->setRelation('client', $client);
|
||||||
|
$invoice->setRelation('company', $subscription->company);
|
||||||
|
$invoice->load('client');
|
||||||
|
$invoice->line_items = $this->generateLineItems($subscription);
|
||||||
|
|
||||||
|
$data['price'] = $invoice->calc()->getTotal();
|
||||||
|
|
||||||
|
$invoice->discount = $subscription->promo_discount;
|
||||||
|
$invoice->is_amount_discount = $subscription->is_amount_discount;
|
||||||
|
|
||||||
|
$data['promo_price'] = $invoice->calc()->getTotal();
|
||||||
|
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateLineItems($subscription, $is_recurring = false)
|
||||||
|
{
|
||||||
|
|
||||||
|
$line_items = [];
|
||||||
|
|
||||||
|
if(!$is_recurring)
|
||||||
|
{
|
||||||
|
foreach($subscription->service()->products() as $product)
|
||||||
|
{
|
||||||
|
$line_items[] = (array)$this->makeLineItem($product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($subscription->service()->recurring_products() as $product)
|
||||||
|
{
|
||||||
|
$line_items[] = (array)$this->makeLineItem($product);
|
||||||
|
}
|
||||||
|
|
||||||
|
$line_items = $this->cleanItems($line_items);
|
||||||
|
|
||||||
|
return $line_items;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeLineItem($product)
|
||||||
|
{
|
||||||
|
$item = new InvoiceItem;
|
||||||
|
$item->quantity = $product->quantity;
|
||||||
|
$item->product_key = $product->product_key;
|
||||||
|
$item->notes = $product->notes;
|
||||||
|
$item->cost = $product->price;
|
||||||
|
$item->tax_rate1 = $product->tax_rate1 ?: 0;
|
||||||
|
$item->tax_name1 = $product->tax_name1 ?: '';
|
||||||
|
$item->tax_rate2 = $product->tax_rate2 ?: 0;
|
||||||
|
$item->tax_name2 = $product->tax_name2 ?: '';
|
||||||
|
$item->tax_rate3 = $product->tax_rate3 ?: 0;
|
||||||
|
$item->tax_name3 = $product->tax_name3 ?: '';
|
||||||
|
$item->custom_value1 = $product->custom_value1 ?: '';
|
||||||
|
$item->custom_value2 = $product->custom_value2 ?: '';
|
||||||
|
$item->custom_value3 = $product->custom_value3 ?: '';
|
||||||
|
$item->custom_value4 = $product->custom_value4 ?: '';
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
}
|
}
|
@ -14,6 +14,7 @@ namespace App\Services\Invoice;
|
|||||||
use App\Jobs\Entity\CreateEntityPdf;
|
use App\Jobs\Entity\CreateEntityPdf;
|
||||||
use App\Jobs\Invoice\InvoiceWorkflowSettings;
|
use App\Jobs\Invoice\InvoiceWorkflowSettings;
|
||||||
use App\Jobs\Util\UnlinkFile;
|
use App\Jobs\Util\UnlinkFile;
|
||||||
|
use App\Libraries\Currency\Conversion\CurrencyApi;
|
||||||
use App\Models\CompanyGateway;
|
use App\Models\CompanyGateway;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
@ -62,7 +63,25 @@ class InvoiceService
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the exchange rate on the invoice if the client currency
|
||||||
|
* is different to the company currency.
|
||||||
|
*/
|
||||||
|
public function setExchangeRate()
|
||||||
|
{
|
||||||
|
|
||||||
|
$client_currency = $this->invoice->client->getSetting('currency_id');
|
||||||
|
$company_currency = $this->invoice->company->settings->currency_id;
|
||||||
|
|
||||||
|
if ($company_currency != $client_currency) {
|
||||||
|
|
||||||
|
$exchange_rate = new CurrencyApi();
|
||||||
|
|
||||||
|
$this->invoice->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, now());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Applies the recurring invoice number.
|
* Applies the recurring invoice number.
|
||||||
* @return $this InvoiceService object
|
* @return $this InvoiceService object
|
||||||
@ -132,6 +151,8 @@ class InvoiceService
|
|||||||
{
|
{
|
||||||
$this->invoice = (new MarkSent($this->invoice->client, $this->invoice))->run();
|
$this->invoice = (new MarkSent($this->invoice->client, $this->invoice))->run();
|
||||||
|
|
||||||
|
$this->setExchangeRate();
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,11 +310,12 @@ class Design extends BaseDesign
|
|||||||
$aliases = [
|
$aliases = [
|
||||||
'$product.product_key' => '$product.item',
|
'$product.product_key' => '$product.item',
|
||||||
'$task.product_key' => '$task.service',
|
'$task.product_key' => '$task.service',
|
||||||
|
'$task.rate' => '$task.cost',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($this->context['pdf_variables']["{$type}_columns"] as $column) {
|
foreach ($this->context['pdf_variables']["{$type}_columns"] as $column) {
|
||||||
if (array_key_exists($column, $aliases)) {
|
if (array_key_exists($column, $aliases)) {
|
||||||
$elements[] = ['element' => 'th', 'content' => $aliases[$column] . '_label', 'properties' => ['hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]];
|
$elements[] = ['element' => 'th', 'content' => $aliases[$column] . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($aliases[$column], 1) . '-th', 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]];
|
||||||
} elseif ($column == '$product.discount' && !$this->client->company->enable_product_discount) {
|
} elseif ($column == '$product.discount' && !$this->client->company->enable_product_discount) {
|
||||||
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']];
|
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']];
|
||||||
} elseif ($column == '$product.quantity' && !$this->client->company->enable_product_quantity) {
|
} elseif ($column == '$product.quantity' && !$this->client->company->enable_product_quantity) {
|
||||||
|
@ -14,6 +14,7 @@ namespace App\Services\Quote;
|
|||||||
use App\Events\Quote\QuoteWasMarkedSent;
|
use App\Events\Quote\QuoteWasMarkedSent;
|
||||||
use App\Models\Quote;
|
use App\Models\Quote;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class MarkSent
|
class MarkSent
|
||||||
{
|
{
|
||||||
@ -37,6 +38,13 @@ class MarkSent
|
|||||||
|
|
||||||
$this->quote->markInvitationsSent();
|
$this->quote->markInvitationsSent();
|
||||||
|
|
||||||
|
if ($this->quote->due_date != '' || $this->quote->client->getSetting('valid_until') == '') {
|
||||||
|
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->quote->due_date = Carbon::parse($this->quote->date)->addDays($this->quote->client->getSetting('valid_until'));
|
||||||
|
}
|
||||||
|
|
||||||
event(new QuoteWasMarkedSent($this->quote, $this->quote->company, Ninja::eventVars()));
|
event(new QuoteWasMarkedSent($this->quote, $this->quote->company, Ninja::eventVars()));
|
||||||
|
|
||||||
$this->quote
|
$this->quote
|
||||||
|
@ -14,15 +14,19 @@ namespace App\Services\Subscription;
|
|||||||
use App\DataMapper\InvoiceItem;
|
use App\DataMapper\InvoiceItem;
|
||||||
use App\Factory\InvoiceFactory;
|
use App\Factory\InvoiceFactory;
|
||||||
use App\Factory\InvoiceToRecurringInvoiceFactory;
|
use App\Factory\InvoiceToRecurringInvoiceFactory;
|
||||||
|
use App\Factory\RecurringInvoiceFactory;
|
||||||
use App\Jobs\Util\SystemLogger;
|
use App\Jobs\Util\SystemLogger;
|
||||||
use App\Models\Subscription;
|
|
||||||
use App\Models\ClientContact;
|
use App\Models\ClientContact;
|
||||||
use App\Models\ClientSubscription;
|
use App\Models\ClientSubscription;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\PaymentHash;
|
use App\Models\PaymentHash;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
|
use App\Models\RecurringInvoice;
|
||||||
|
use App\Models\Subscription;
|
||||||
use App\Models\SystemLog;
|
use App\Models\SystemLog;
|
||||||
use App\Repositories\InvoiceRepository;
|
use App\Repositories\InvoiceRepository;
|
||||||
|
use App\Repositories\RecurringInvoiceRepository;
|
||||||
|
use App\Repositories\SubscriptionRepository;
|
||||||
use App\Utils\Traits\CleanLineItems;
|
use App\Utils\Traits\CleanLineItems;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use GuzzleHttp\RequestOptions;
|
use GuzzleHttp\RequestOptions;
|
||||||
@ -36,7 +40,7 @@ class SubscriptionService
|
|||||||
private $subscription;
|
private $subscription;
|
||||||
|
|
||||||
/** @var client_subscription */
|
/** @var client_subscription */
|
||||||
private $client_subscription;
|
// private $client_subscription;
|
||||||
|
|
||||||
public function __construct(Subscription $subscription)
|
public function __construct(Subscription $subscription)
|
||||||
{
|
{
|
||||||
@ -50,15 +54,41 @@ class SubscriptionService
|
|||||||
throw new \Exception("Illegal entrypoint into method, payload must contain billing context");
|
throw new \Exception("Illegal entrypoint into method, payload must contain billing context");
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point we have some state carried from the billing page
|
// if we have a recurring product - then generate a recurring invoice
|
||||||
// to this, available as $payment_hash->data->billing_context. Make something awesome ⭐
|
if(strlen($this->subscription->recurring_product_ids) >=1){
|
||||||
|
|
||||||
// create client subscription record
|
$recurring_invoice = $this->convertInvoiceToRecurring($payment_hash->payment->client_id);
|
||||||
//
|
$recurring_invoice_repo = new RecurringInvoiceRepository();
|
||||||
// create recurring invoice if is_recurring
|
|
||||||
//
|
|
||||||
|
|
||||||
|
$recurring_invoice->next_send_date = now();
|
||||||
|
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
|
||||||
|
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
|
||||||
|
|
||||||
|
/* Start the recurring service */
|
||||||
|
$recurring_invoice->service()
|
||||||
|
->start()
|
||||||
|
->save();
|
||||||
|
|
||||||
|
//execute any webhooks
|
||||||
|
$this->triggerWebhook();
|
||||||
|
|
||||||
|
if(array_key_exists('post_purchase_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['post_purchase_url']) >=1)
|
||||||
|
return redirect($this->subscription->webhook_configuration['post_purchase_url']);
|
||||||
|
|
||||||
|
return redirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
//execute any webhooks
|
||||||
|
$this->triggerWebhook();
|
||||||
|
|
||||||
|
if(array_key_exists('post_purchase_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['post_purchase_url']) >=1)
|
||||||
|
return redirect($this->subscription->webhook_configuration['post_purchase_url']);
|
||||||
|
|
||||||
|
return redirect('/client/invoices/'.$this->encodePrimaryKey($payment_hash->fee_invoice_id));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,192 +99,139 @@ class SubscriptionService
|
|||||||
public function startTrial(array $data)
|
public function startTrial(array $data)
|
||||||
{
|
{
|
||||||
// Redirects from here work just fine. Livewire will respect it.
|
// Redirects from here work just fine. Livewire will respect it.
|
||||||
|
$client_contact = ClientContact::find($data['contact_id']);
|
||||||
|
|
||||||
if(!$this->subscription->trial_enabled)
|
if(!$this->subscription->trial_enabled)
|
||||||
return new \Exception("Trials are disabled for this product");
|
return new \Exception("Trials are disabled for this product");
|
||||||
|
|
||||||
$contact = ClientContact::with('client')->find($data['contact_id']);
|
//create recurring invoice with start date = trial_duration + 1 day
|
||||||
|
$recurring_invoice_repo = new RecurringInvoiceRepository();
|
||||||
|
|
||||||
$cs = new ClientSubscription();
|
$recurring_invoice = $this->convertInvoiceToRecurring($client_contact->client_id);
|
||||||
$cs->subscription_id = $this->subscription->id;
|
$recurring_invoice->next_send_date = now()->addSeconds($this->subscription->trial_duration);
|
||||||
$cs->company_id = $this->subscription->company_id;
|
$recurring_invoice->backup = 'is_trial';
|
||||||
$cs->trial_started = time();
|
|
||||||
$cs->trial_ends = time() + $this->subscription->trial_duration;
|
|
||||||
$cs->quantity = $data['quantity'];
|
|
||||||
$cs->client_id = $contact->client->id;
|
|
||||||
$cs->save();
|
|
||||||
|
|
||||||
$this->client_subscription = $cs;
|
if(array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0)
|
||||||
|
{
|
||||||
|
$recurring_invoice->discount = $this->subscription->promo_discount;
|
||||||
|
$recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$recurring_invoice = $recurring_invoice_repo->save($data, $recurring_invoice);
|
||||||
|
|
||||||
|
/* Start the recurring service */
|
||||||
|
$recurring_invoice->service()
|
||||||
|
->start()
|
||||||
|
->save();
|
||||||
|
|
||||||
//execute any webhooks
|
//execute any webhooks
|
||||||
$this->triggerWebhook();
|
$this->triggerWebhook();
|
||||||
|
|
||||||
if(strlen($this->subscription->webhook_configuration->post_purchase_url) >=1)
|
if(array_key_exists('post_purchase_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['post_purchase_url']) >=1)
|
||||||
return redirect($this->subscription->webhook_configuration->post_purchase_url);
|
return redirect($this->subscription->webhook_configuration['post_purchase_url']);
|
||||||
|
|
||||||
return redirect('/client/subscription/'.$cs->hashed_id);
|
return redirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function createInvoice($data): ?\App\Models\Invoice
|
public function createInvoice($data): ?\App\Models\Invoice
|
||||||
{
|
{
|
||||||
|
|
||||||
$invoice_repo = new InvoiceRepository();
|
$invoice_repo = new InvoiceRepository();
|
||||||
|
$subscription_repo = new SubscriptionRepository();
|
||||||
|
|
||||||
$data['line_items'] = $this->cleanItems($this->createLineItems($data));
|
$invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
|
||||||
|
$invoice->line_items = $subscription_repo->generateLineItems($this->subscription);
|
||||||
|
$invoice->subscription_id = $this->subscription->id;
|
||||||
|
|
||||||
return $invoice_repo->save($data, InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id));
|
if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0)
|
||||||
|
{
|
||||||
}
|
$invoice->discount = $this->subscription->promo_discount;
|
||||||
|
$invoice->is_amount_discount = $this->subscription->is_amount_discount;
|
||||||
/**
|
|
||||||
* Creates the required line items for the invoice
|
|
||||||
* for the billing subscription.
|
|
||||||
*/
|
|
||||||
private function createLineItems($data): array
|
|
||||||
{
|
|
||||||
|
|
||||||
$line_items = [];
|
|
||||||
|
|
||||||
$product = $this->subscription->product;
|
|
||||||
|
|
||||||
$item = new InvoiceItem;
|
|
||||||
$item->quantity = $data['quantity'];
|
|
||||||
$item->product_key = $product->product_key;
|
|
||||||
$item->notes = $product->notes;
|
|
||||||
$item->cost = $product->price;
|
|
||||||
$item->tax_rate1 = $product->tax_rate1 ?: 0;
|
|
||||||
$item->tax_name1 = $product->tax_name1 ?: '';
|
|
||||||
$item->tax_rate2 = $product->tax_rate2 ?: 0;
|
|
||||||
$item->tax_name2 = $product->tax_name2 ?: '';
|
|
||||||
$item->tax_rate3 = $product->tax_rate3 ?: 0;
|
|
||||||
$item->tax_name3 = $product->tax_name3 ?: '';
|
|
||||||
$item->custom_value1 = $product->custom_value1 ?: '';
|
|
||||||
$item->custom_value2 = $product->custom_value2 ?: '';
|
|
||||||
$item->custom_value3 = $product->custom_value3 ?: '';
|
|
||||||
$item->custom_value4 = $product->custom_value4 ?: '';
|
|
||||||
|
|
||||||
//$item->type_id need to switch whether the subscription is a service or product
|
|
||||||
|
|
||||||
$line_items[] = $item;
|
|
||||||
|
|
||||||
|
|
||||||
//do we have a promocode? enter this as a line item.
|
|
||||||
if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0)
|
|
||||||
$line_items[] = $this->createPromoLine($data);
|
|
||||||
|
|
||||||
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->subscription->product;
|
|
||||||
$discounted_amount = 0;
|
|
||||||
$discount = 0;
|
|
||||||
$amount = $data['quantity'] * $product->cost;
|
|
||||||
|
|
||||||
if ($this->subscription->is_amount_discount == true) {
|
|
||||||
$discount = $this->subscription->promo_discount;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$discount = round($amount * ($this->subscription->promo_discount / 100), 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$discounted_amount = $amount - $discount;
|
return $invoice_repo->save($data, $invoice);
|
||||||
|
|
||||||
$item = new InvoiceItem;
|
|
||||||
$item->quantity = 1;
|
|
||||||
$item->product_key = ctrans('texts.promo_code');
|
|
||||||
$item->notes = ctrans('texts.promo_code');
|
|
||||||
$item->cost = $discounted_amount;
|
|
||||||
$item->tax_rate1 = $product->tax_rate1 ?: 0;
|
|
||||||
$item->tax_name1 = $product->tax_name1 ?: '';
|
|
||||||
$item->tax_rate2 = $product->tax_rate2 ?: 0;
|
|
||||||
$item->tax_name2 = $product->tax_name2 ?: '';
|
|
||||||
$item->tax_rate3 = $product->tax_rate3 ?: 0;
|
|
||||||
$item->tax_name3 = $product->tax_name3 ?: '';
|
|
||||||
|
|
||||||
return $item;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function convertInvoiceToRecurring($payment_hash)
|
|
||||||
{
|
|
||||||
//The first invoice is a plain invoice - the second is fired on the recurring schedule.
|
|
||||||
$invoice = Invoice::find($payment_hash->billing_context->invoice_id);
|
|
||||||
|
|
||||||
if(!$invoice)
|
|
||||||
throw new \Exception("Could not match an invoice for payment of billing subscription");
|
|
||||||
|
|
||||||
//todo - need to remove the promo code - if it exists
|
private function convertInvoiceToRecurring($client_id)
|
||||||
|
|
||||||
return InvoiceToRecurringInvoiceFactory::create($invoice);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createClientSubscription($payment_hash)
|
|
||||||
{
|
{
|
||||||
|
|
||||||
//is this a recurring or one off subscription.
|
$subscription_repo = new SubscriptionRepository();
|
||||||
|
|
||||||
$cs = new ClientSubscription();
|
$recurring_invoice = RecurringInvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
|
||||||
$cs->subscription_id = $this->subscription->id;
|
$recurring_invoice->client_id = $client_id;
|
||||||
$cs->company_id = $this->subscription->company_id;
|
$recurring_invoice->line_items = $subscription_repo->generateLineItems($this->subscription, true);
|
||||||
|
$recurring_invoice->subscription_id = $this->subscription->id;
|
||||||
$cs->invoice_id = $payment_hash->billing_context->invoice_id;
|
$recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY;
|
||||||
$cs->client_id = $payment_hash->billing_context->client_id;
|
$recurring_invoice->date = now();
|
||||||
$cs->quantity = $payment_hash->billing_context->quantity;
|
$recurring_invoice->remaining_cycles = -1;
|
||||||
|
|
||||||
//if is_recurring
|
|
||||||
//create recurring invoice from invoice
|
|
||||||
if($this->subscription->is_recurring)
|
|
||||||
{
|
|
||||||
$recurring_invoice = $this->convertInvoiceToRecurring($payment_hash);
|
|
||||||
$recurring_invoice->frequency_id = $this->subscription->frequency_id;
|
|
||||||
$recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d'));
|
|
||||||
$recurring_invoice->save();
|
|
||||||
$cs->recurring_invoice_id = $recurring_invoice->id;
|
|
||||||
|
|
||||||
//?set the recurring invoice as active - set the date here also based on the frequency?
|
|
||||||
$recurring_invoice->service()->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$cs->save();
|
|
||||||
|
|
||||||
$this->client_subscription = $cs;
|
|
||||||
|
|
||||||
|
return $recurring_invoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @deprecated due to change in architecture
|
||||||
|
|
||||||
|
// public function createClientSubscription($payment_hash)
|
||||||
|
// {
|
||||||
|
|
||||||
|
// //is this a recurring or one off subscription.
|
||||||
|
|
||||||
|
// $cs = new ClientSubscription();
|
||||||
|
// $cs->subscription_id = $this->subscription->id;
|
||||||
|
// $cs->company_id = $this->subscription->company_id;
|
||||||
|
|
||||||
|
// $cs->invoice_id = $payment_hash->billing_context->invoice_id;
|
||||||
|
// $cs->client_id = $payment_hash->billing_context->client_id;
|
||||||
|
// $cs->quantity = $payment_hash->billing_context->quantity;
|
||||||
|
|
||||||
|
// //if is_recurring
|
||||||
|
// //create recurring invoice from invoice
|
||||||
|
// if($this->subscription->is_recurring)
|
||||||
|
// {
|
||||||
|
// $recurring_invoice = $this->convertInvoiceToRecurring($payment_hash);
|
||||||
|
// $recurring_invoice->frequency_id = $this->subscription->frequency_id;
|
||||||
|
// $recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d'));
|
||||||
|
// $recurring_invoice->save();
|
||||||
|
// $cs->recurring_invoice_id = $recurring_invoice->id;
|
||||||
|
|
||||||
|
// //?set the recurring invoice as active - set the date here also based on the frequency?
|
||||||
|
// $recurring_invoice->service()->start();
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// $cs->save();
|
||||||
|
|
||||||
|
// $this->client_subscription = $cs;
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
//@todo - need refactor
|
||||||
public function triggerWebhook()
|
public function triggerWebhook()
|
||||||
{
|
{
|
||||||
//hit the webhook to after a successful onboarding
|
//hit the webhook to after a successful onboarding
|
||||||
|
|
||||||
$body = [
|
// $body = [
|
||||||
'subscription' => $this->subscription,
|
// 'subscription' => $this->subscription,
|
||||||
'client_subscription' => $this->client_subscription,
|
// 'client_subscription' => $this->client_subscription,
|
||||||
'client' => $this->client_subscription->client->toArray(),
|
// 'client' => $this->client_subscription->client->toArray(),
|
||||||
];
|
// ];
|
||||||
|
|
||||||
|
|
||||||
$client = new \GuzzleHttp\Client(['headers' => $this->subscription->webhook_configuration->post_purchase_headers]);
|
// $client = new \GuzzleHttp\Client(['headers' => $this->subscription->webhook_configuration->post_purchase_headers]);
|
||||||
|
|
||||||
$response = $client->{$this->subscription->webhook_configuration->post_purchase_rest_method}($this->subscription->post_purchase_url,[
|
// $response = $client->{$this->subscription->webhook_configuration->post_purchase_rest_method}($this->subscription->post_purchase_url,[
|
||||||
RequestOptions::JSON => ['body' => $body]
|
// RequestOptions::JSON => ['body' => $body]
|
||||||
]);
|
// ]);
|
||||||
|
|
||||||
SystemLogger::dispatch(
|
// SystemLogger::dispatch(
|
||||||
$body,
|
// $body,
|
||||||
SystemLog::CATEGORY_WEBHOOK,
|
// SystemLog::CATEGORY_WEBHOOK,
|
||||||
SystemLog::EVENT_WEBHOOK_RESPONSE,
|
// SystemLog::EVENT_WEBHOOK_RESPONSE,
|
||||||
SystemLog::TYPE_WEBHOOK_RESPONSE,
|
// SystemLog::TYPE_WEBHOOK_RESPONSE,
|
||||||
$this->client_subscription->client,
|
// $this->client_subscription->client,
|
||||||
);
|
// );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,5 +240,13 @@ class SubscriptionService
|
|||||||
//scan for any notification we are required to send
|
//scan for any notification we are required to send
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function products()
|
||||||
|
{
|
||||||
|
return Product::whereIn('id', $this->transformKeys(explode(",", $this->subscription->product_ids)))->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recurring_products()
|
||||||
|
{
|
||||||
|
return Product::whereIn('id', $this->transformKeys(explode(",", $this->subscription->recurring_product_ids)))->get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ class CompanyTransformer extends EntityTransformer
|
|||||||
'system_logs',
|
'system_logs',
|
||||||
'expense_categories',
|
'expense_categories',
|
||||||
'task_statuses',
|
'task_statuses',
|
||||||
'billing_subscriptions',
|
'subscriptions',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -366,6 +366,6 @@ class CompanyTransformer extends EntityTransformer
|
|||||||
{
|
{
|
||||||
$transformer = new SubscriptionTransformer($this->serializer);
|
$transformer = new SubscriptionTransformer($this->serializer);
|
||||||
|
|
||||||
return $this->includeCollection($company->billing_subscriptions, $transformer, Subscription::class);
|
return $this->includeCollection($company->subscriptions, $transformer, Subscription::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,6 +139,7 @@ class RecurringInvoiceTransformer extends EntityTransformer
|
|||||||
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
|
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
|
||||||
'due_date_days' => (string) $invoice->due_date_days ?: '',
|
'due_date_days' => (string) $invoice->due_date_days ?: '',
|
||||||
'paid_to_date' => (float) $invoice->paid_to_date,
|
'paid_to_date' => (float) $invoice->paid_to_date,
|
||||||
|
'subscription_id' => (string)$this->encodePrimaryKey($invoice->subscription_id),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,32 +33,34 @@ class SubscriptionTransformer extends EntityTransformer
|
|||||||
|
|
||||||
public function transform(Subscription $subscription): array
|
public function transform(Subscription $subscription): array
|
||||||
{
|
{
|
||||||
$std = new \stdClass;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $this->encodePrimaryKey($subscription->id),
|
'id' => $this->encodePrimaryKey($subscription->id),
|
||||||
'user_id' => $this->encodePrimaryKey($subscription->user_id),
|
'user_id' => $this->encodePrimaryKey($subscription->user_id),
|
||||||
'product_id' => $this->encodePrimaryKey($subscription->product_id),
|
'group_id' => $this->encodePrimaryKey($subscription->group_id),
|
||||||
|
'product_ids' => (string)$subscription->product_ids,
|
||||||
|
'name' => (string)$subscription->name,
|
||||||
|
'recurring_product_ids' => (string)$subscription->recurring_product_ids,
|
||||||
'assigned_user_id' => $this->encodePrimaryKey($subscription->assigned_user_id),
|
'assigned_user_id' => $this->encodePrimaryKey($subscription->assigned_user_id),
|
||||||
'company_id' => $this->encodePrimaryKey($subscription->company_id),
|
'company_id' => $this->encodePrimaryKey($subscription->company_id),
|
||||||
'is_recurring' => (bool)$subscription->is_recurring,
|
'price' => (float) $subscription->price,
|
||||||
|
'promo_price' => (float) $subscription->promo_price,
|
||||||
'frequency_id' => (string)$subscription->frequency_id,
|
'frequency_id' => (string)$subscription->frequency_id,
|
||||||
'auto_bill' => (string)$subscription->auto_bill,
|
'auto_bill' => (string)$subscription->auto_bill,
|
||||||
'promo_code' => (string)$subscription->promo_code,
|
'promo_code' => (string)$subscription->promo_code,
|
||||||
'promo_discount' => (float)$subscription->promo_discount,
|
'promo_discount' => (float)$subscription->promo_discount,
|
||||||
'is_amount_discount' => (bool)$subscription->is_amount_discount,
|
'is_amount_discount' => (bool)$subscription->is_amount_discount,
|
||||||
'allow_cancellation' => (bool)$subscription->allow_cancellation,
|
'allow_cancellation' => (bool)$subscription->allow_cancellation,
|
||||||
'per_seat_enabled' => (bool)$subscription->per_set_enabled,
|
'per_seat_enabled' => (bool)$subscription->per_seat_enabled,
|
||||||
'min_seats_limit' => (int)$subscription->min_seats_limit,
|
|
||||||
'max_seats_limit' => (int)$subscription->max_seats_limit,
|
'max_seats_limit' => (int)$subscription->max_seats_limit,
|
||||||
'trial_enabled' => (bool)$subscription->trial_enabled,
|
'trial_enabled' => (bool)$subscription->trial_enabled,
|
||||||
'trial_duration' => (int)$subscription->trial_duration,
|
'trial_duration' => (int)$subscription->trial_duration,
|
||||||
'allow_query_overrides' => (bool)$subscription->allow_query_overrides,
|
'allow_query_overrides' => (bool)$subscription->allow_query_overrides,
|
||||||
'allow_plan_changes' => (bool)$subscription->allow_plan_changes,
|
'allow_plan_changes' => (bool)$subscription->allow_plan_changes,
|
||||||
'plan_map' => (string)$subscription->plan_map,
|
|
||||||
'refund_period' => (int)$subscription->refund_period,
|
'refund_period' => (int)$subscription->refund_period,
|
||||||
'webhook_configuration' => $subscription->webhook_configuration ?: $std,
|
'webhook_configuration' => $subscription->webhook_configuration ?: [],
|
||||||
'purchase_page' => (string)route('client.subscription.purchase', $subscription->hashed_id),
|
'purchase_page' => (string)route('client.subscription.purchase', $subscription->hashed_id),
|
||||||
|
'currency_id' => (string) $subscription->currency_id,
|
||||||
'is_deleted' => (bool)$subscription->is_deleted,
|
'is_deleted' => (bool)$subscription->is_deleted,
|
||||||
'created_at' => (int)$subscription->created_at,
|
'created_at' => (int)$subscription->created_at,
|
||||||
'updated_at' => (int)$subscription->updated_at,
|
'updated_at' => (int)$subscription->updated_at,
|
||||||
|
30
app/Utils/ClientPortal/MagicLink.php
Normal file
30
app/Utils/ClientPortal/MagicLink.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?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\Utils\ClientPortal;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class MagicLink
|
||||||
|
{
|
||||||
|
|
||||||
|
//return a magic login link URL
|
||||||
|
public static function create($email, $url = null) :string
|
||||||
|
{
|
||||||
|
$magic_key = Str::random(64);
|
||||||
|
$timeout = 600; //seconds
|
||||||
|
|
||||||
|
Cache::add($magic_key, $email, $timeout);
|
||||||
|
|
||||||
|
return route('client.contact_magic_link', ['magic_link' => $magic_key, 'redirect' => $url]);
|
||||||
|
}
|
||||||
|
}
|
@ -160,8 +160,8 @@ class HtmlEngine
|
|||||||
$data['$invoice.subtotal'] = &$data['$subtotal'];
|
$data['$invoice.subtotal'] = &$data['$subtotal'];
|
||||||
|
|
||||||
if ($this->entity->partial > 0) {
|
if ($this->entity->partial > 0) {
|
||||||
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: ' ', 'label' => ctrans('texts.partial_due')];
|
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: ' ', 'label' => ctrans('texts.balance_due')];
|
||||||
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
|
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.balance_due')];
|
||||||
} else {
|
} else {
|
||||||
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: ' ', 'label' => ctrans('texts.balance_due')];
|
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: ' ', 'label' => ctrans('texts.balance_due')];
|
||||||
$data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
|
$data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
|
||||||
@ -192,6 +192,8 @@ class HtmlEngine
|
|||||||
$data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: ' ', 'label' => ctrans('texts.taxes')];
|
$data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: ' ', 'label' => ctrans('texts.taxes')];
|
||||||
$data['$invoice.taxes'] = &$data['$taxes'];
|
$data['$invoice.taxes'] = &$data['$taxes'];
|
||||||
|
|
||||||
|
$data['$user.name'] = ['value' => $this->entity->user->present()->name(), 'label' => ctrans('texts.name')];
|
||||||
|
$data['$user_iban'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
|
||||||
$data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
|
$data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
|
||||||
$data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
|
$data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
|
||||||
$data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
|
$data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
|
||||||
@ -259,7 +261,7 @@ class HtmlEngine
|
|||||||
$data['$contact.email'] = ['value' => $this->contact->email, 'label' => ctrans('texts.email')];
|
$data['$contact.email'] = ['value' => $this->contact->email, 'label' => ctrans('texts.email')];
|
||||||
$data['$contact.phone'] = ['value' => $this->contact->phone, 'label' => ctrans('texts.phone')];
|
$data['$contact.phone'] = ['value' => $this->contact->phone, 'label' => ctrans('texts.phone')];
|
||||||
|
|
||||||
$data['$contact.name'] = ['value' => isset($this->contact) ? $this->contact->present()->name() : 'no contact name on record', 'label' => ctrans('texts.contact_name')];
|
$data['$contact.name'] = ['value' => isset($this->contact) ? $this->contact->present()->name() : $this->client->present()->name(), 'label' => ctrans('texts.contact_name')];
|
||||||
$data['$contact.first_name'] = ['value' => isset($this->contact) ? $this->contact->first_name : '', 'label' => ctrans('texts.first_name')];
|
$data['$contact.first_name'] = ['value' => isset($this->contact) ? $this->contact->first_name : '', 'label' => ctrans('texts.first_name')];
|
||||||
$data['$contact.last_name'] = ['value' => isset($this->contact) ? $this->contact->last_name : '', 'label' => ctrans('texts.last_name')];
|
$data['$contact.last_name'] = ['value' => isset($this->contact) ? $this->contact->last_name : '', 'label' => ctrans('texts.last_name')];
|
||||||
|
|
||||||
@ -287,7 +289,7 @@ class HtmlEngine
|
|||||||
|
|
||||||
$data['$signature'] = ['value' => $this->settings->email_signature ?: ' ', 'label' => ''];
|
$data['$signature'] = ['value' => $this->settings->email_signature ?: ' ', 'label' => ''];
|
||||||
|
|
||||||
$data['$spc_qr_code'] = ['value' => $this->company->present()->getSpcQrCode($this->client->currency()->code, $this->entity->number, $this->entity->balance), 'label' => ''];
|
$data['$spc_qr_code'] = ['value' => $this->company->present()->getSpcQrCode($this->client->currency()->code, $this->entity->number, $this->entity->balance, $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client)), 'label' => ''];
|
||||||
|
|
||||||
$logo = $this->company->present()->logo($this->settings);
|
$logo = $this->company->present()->logo($this->settings);
|
||||||
|
|
||||||
@ -327,6 +329,7 @@ class HtmlEngine
|
|||||||
$data['$task.service'] = ['value' => '', 'label' => ctrans('texts.service')];
|
$data['$task.service'] = ['value' => '', 'label' => ctrans('texts.service')];
|
||||||
$data['$task.description'] = ['value' => '', 'label' => ctrans('texts.description')];
|
$data['$task.description'] = ['value' => '', 'label' => ctrans('texts.description')];
|
||||||
$data['$task.rate'] = ['value' => '', 'label' => ctrans('texts.rate')];
|
$data['$task.rate'] = ['value' => '', 'label' => ctrans('texts.rate')];
|
||||||
|
$data['$task.cost'] = ['value' => '', 'label' => ctrans('texts.rate')];
|
||||||
$data['$task.hours'] = ['value' => '', 'label' => ctrans('texts.hours')];
|
$data['$task.hours'] = ['value' => '', 'label' => ctrans('texts.hours')];
|
||||||
$data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
$data['$task.tax'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
||||||
$data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
$data['$task.tax_name1'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
||||||
@ -334,6 +337,10 @@ class HtmlEngine
|
|||||||
$data['$task.tax_name3'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
$data['$task.tax_name3'] = ['value' => '', 'label' => ctrans('texts.tax')];
|
||||||
$data['$task.line_total'] = ['value' => '', 'label' => ctrans('texts.line_total')];
|
$data['$task.line_total'] = ['value' => '', 'label' => ctrans('texts.line_total')];
|
||||||
$data['$task.service'] = ['value' => '', 'label' => ctrans('texts.service')];
|
$data['$task.service'] = ['value' => '', 'label' => ctrans('texts.service')];
|
||||||
|
$data['$task.task1'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task1')];
|
||||||
|
$data['$task.task2'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task2')];
|
||||||
|
$data['$task.task3'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task3')];
|
||||||
|
$data['$task.task4'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task4')];
|
||||||
|
|
||||||
if ($this->settings->signature_on_pdf) {
|
if ($this->settings->signature_on_pdf) {
|
||||||
$data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')];
|
$data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')];
|
||||||
|
@ -79,6 +79,7 @@ class SystemHealth
|
|||||||
'exec' => (bool)self::checkExecWorks(),
|
'exec' => (bool)self::checkExecWorks(),
|
||||||
'open_basedir' => (bool)self::checkOpenBaseDir(),
|
'open_basedir' => (bool)self::checkOpenBaseDir(),
|
||||||
'mail_mailer' => (string)self::checkMailMailer(),
|
'mail_mailer' => (string)self::checkMailMailer(),
|
||||||
|
'flutter_renderer' => (string)config('ninja.flutter_canvas_kit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +367,7 @@ trait MakesInvoiceValues
|
|||||||
|
|
||||||
$replacements = [
|
$replacements = [
|
||||||
'literal' => [
|
'literal' => [
|
||||||
':MONTH' => now()->localeMonth,
|
':MONTH' => Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F'),
|
||||||
':YEAR' => now()->year,
|
':YEAR' => now()->year,
|
||||||
':QUARTER' => 'Q' . now()->quarter,
|
':QUARTER' => 'Q' . now()->quarter,
|
||||||
],
|
],
|
||||||
|
@ -38,8 +38,6 @@ trait UserNotifies
|
|||||||
array_push($notifiable_methods, 'mail');
|
array_push($notifiable_methods, 'mail');
|
||||||
}
|
}
|
||||||
|
|
||||||
nlog($notifiable_methods);
|
|
||||||
|
|
||||||
// if(count(array_intersect($required_permissions, $notifications->slack)) >=1)
|
// if(count(array_intersect($required_permissions, $notifications->slack)) >=1)
|
||||||
// array_push($notifiable_methods, 'slack');
|
// array_push($notifiable_methods, 'slack');
|
||||||
|
|
||||||
@ -63,10 +61,11 @@ nlog($notifiable_methods);
|
|||||||
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(['all_user_notifications'], $notifications->email)) >= 1 || count(array_intersect(['all_notifications'],$notifications->email)) >= 1) {
|
||||||
array_push($notifiable_methods, 'mail');
|
array_push($notifiable_methods, 'mail');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return $notifiable_methods;
|
return $notifiable_methods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +89,8 @@
|
|||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "app/",
|
"App\\": "app/",
|
||||||
"Database\\Factories\\": "database/factories/",
|
"Database\\Factories\\": "database/factories/",
|
||||||
"Database\\Seeders\\": "database/seeders/"
|
"Database\\Seeders\\": "database/seeders/",
|
||||||
|
"Modules\\": "Modules/"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
]
|
]
|
||||||
@ -106,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"post-install-cmd": [
|
"post-install-cmd": [
|
||||||
"if [ '${IS_DOCKER:-false}' != 'true' ]; then vendor/bin/snappdf download; fi"
|
"if [ \"${IS_DOCKER:-false}\" != \"true\" ]; then vendor/bin/snappdf download; fi"
|
||||||
],
|
],
|
||||||
"post-update-cmd": [
|
"post-update-cmd": [
|
||||||
"vendor/bin/snappdf download"
|
"vendor/bin/snappdf download"
|
||||||
|
401
composer.lock
generated
401
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'web_url' => 'https://www.invoiceninja.com',
|
'web_url' => 'https://www.invoiceninja.com',
|
||||||
|
'admin_token' => env('NINJA_ADMIN_TOKEN', ''),
|
||||||
'license_url' => 'https://app.invoiceninja.com',
|
'license_url' => 'https://app.invoiceninja.com',
|
||||||
'production' => env('NINJA_PROD', false),
|
'production' => env('NINJA_PROD', false),
|
||||||
'license' => env('NINJA_LICENSE', ''),
|
'license' => env('NINJA_LICENSE', ''),
|
||||||
@ -13,7 +14,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.32',
|
'app_version' => '5.1.33',
|
||||||
'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),
|
||||||
@ -140,8 +141,8 @@ return [
|
|||||||
'log_pdf_html' => env('LOG_PDF_HTML', false),
|
'log_pdf_html' => env('LOG_PDF_HTML', false),
|
||||||
'expanded_logging' => env('EXPANDED_LOGGING', false),
|
'expanded_logging' => env('EXPANDED_LOGGING', false),
|
||||||
'snappdf_chromium_path' => env('SNAPPDF_CHROMIUM_PATH', false),
|
'snappdf_chromium_path' => env('SNAPPDF_CHROMIUM_PATH', false),
|
||||||
'v4_migration_version' => '4.5.31',
|
'v4_migration_version' => '4.5.35',
|
||||||
'flutter_canvas_kit' => env('FLUTTER_CANVAS_KIT', false),
|
'flutter_canvas_kit' => env('FLUTTER_CANVAS_KIT', 'selfhosted-html'),
|
||||||
'webcron_secret' => env('WEBCRON_SECRET', false),
|
'webcron_secret' => env('WEBCRON_SECRET', false),
|
||||||
'disable_auto_update' => env('DISABLE_AUTO_UPDATE', false),
|
'disable_auto_update' => env('DISABLE_AUTO_UPDATE', false),
|
||||||
'invoiceninja_hosted_pdf_generation' => env('NINJA_HOSTED_PDF', false),
|
'invoiceninja_hosted_pdf_generation' => env('NINJA_HOSTED_PDF', false),
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddPriceColumnToSubscriptionsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('subscriptions', function (Blueprint $table) {
|
||||||
|
$table->decimal('price', 20, 6)->default(0);
|
||||||
|
$table->decimal('promo_price', 20, 6)->default(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('recurring_invoices', function (Blueprint $table) {
|
||||||
|
$table->unsignedInteger('subscription_id')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('subscriptions', function (Blueprint $table) {
|
||||||
|
$table->unsignedInteger('group_id')->nullable()->change();
|
||||||
|
$table->text('product_ids')->nullable()->change();
|
||||||
|
$table->text('recurring_product_ids')->nullable()->change();
|
||||||
|
$table->text('auto_bill')->nullable()->change();
|
||||||
|
$table->text('promo_code')->nullable()->change();
|
||||||
|
$table->unsignedInteger('frequency_id')->nullable()->change();
|
||||||
|
$table->text('plan_map')->nullable()->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class ModifyColumnOnSubscriptionsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('subscriptions', function (Blueprint $table) {
|
||||||
|
$table->unsignedInteger('assigned_user_id')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
3
modules_statuses.json
Normal file
3
modules_statuses.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"Admin": true
|
||||||
|
}
|
2
public/css/app.css
vendored
2
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
44
public/flutter_service_worker.js
vendored
44
public/flutter_service_worker.js
vendored
@ -3,36 +3,36 @@ const MANIFEST = 'flutter-app-manifest';
|
|||||||
const TEMP = 'flutter-temp-cache';
|
const TEMP = 'flutter-temp-cache';
|
||||||
const CACHE_NAME = 'flutter-app-cache';
|
const CACHE_NAME = 'flutter-app-cache';
|
||||||
const RESOURCES = {
|
const RESOURCES = {
|
||||||
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
|
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
|
||||||
|
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
|
||||||
|
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
|
||||||
|
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
|
||||||
|
"/": "23224b5e03519aaa87594403d54412cf",
|
||||||
|
"favicon.ico": "51636d3a390451561744c42188ccd628",
|
||||||
|
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
|
||||||
|
"assets/NOTICES": "e80e999afd09f0f14597c78d582d9c7c",
|
||||||
|
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
|
||||||
|
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",
|
||||||
|
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
|
||||||
"assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
|
"assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
|
||||||
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
|
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
|
||||||
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
|
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
|
||||||
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
|
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
|
||||||
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
|
|
||||||
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
|
|
||||||
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
|
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
|
||||||
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
|
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
|
||||||
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
|
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
|
||||||
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
|
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
|
||||||
|
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
|
||||||
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
|
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
|
||||||
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
|
|
||||||
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
|
|
||||||
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
|
|
||||||
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
|
|
||||||
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
|
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
|
||||||
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",
|
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
|
||||||
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
|
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
|
||||||
|
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
|
||||||
|
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
|
||||||
|
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
|
||||||
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
|
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
|
||||||
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
|
"main.dart.js": "4ca4aa873adda14b5d1476f085431b28",
|
||||||
"assets/NOTICES": "e80e999afd09f0f14597c78d582d9c7c",
|
"version.json": "b7c8971e1ab5b627fd2a4317c52b843e"
|
||||||
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
|
|
||||||
"main.dart.js": "44b7ad1fa4ed703c299158cb0bd49af8",
|
|
||||||
"/": "23224b5e03519aaa87594403d54412cf",
|
|
||||||
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
|
|
||||||
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
|
|
||||||
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
|
|
||||||
"version.json": "b7c8971e1ab5b627fd2a4317c52b843e",
|
|
||||||
"favicon.ico": "51636d3a390451561744c42188ccd628"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The application shell files that are downloaded before a service worker can
|
// The application shell files that are downloaded before a service worker can
|
||||||
|
1
public/images/svg/calendar.svg
Normal file
1
public/images/svg/calendar.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
After Width: | Height: | Size: 403 B |
235983
public/main.dart.js
vendored
235983
public/main.dart.js
vendored
File diff suppressed because one or more lines are too long
216479
public/main.foss.dart.js
vendored
Normal file
216479
public/main.foss.dart.js
vendored
Normal file
File diff suppressed because one or more lines are too long
212164
public/main.wasm.dart.js
vendored
Normal file
212164
public/main.wasm.dart.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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=1481aa442df903f3c38b",
|
"/css/app.css": "/css/app.css?id=773c78b0cad68d2f1f2e",
|
||||||
"/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",
|
||||||
|
2
public/vendor/livewire/livewire.js
vendored
2
public/vendor/livewire/livewire.js
vendored
File diff suppressed because one or more lines are too long
2
public/vendor/livewire/livewire.js.map
vendored
2
public/vendor/livewire/livewire.js.map
vendored
File diff suppressed because one or more lines are too long
2
public/vendor/livewire/manifest.json
vendored
2
public/vendor/livewire/manifest.json
vendored
@ -1 +1 @@
|
|||||||
{"/livewire.js":"/livewire.js?id=eb510e851dceb24afd36"}
|
{"/livewire.js":"/livewire.js?id=d9e06c155e467adb5de2"}
|
@ -3968,7 +3968,7 @@ $LANG = array(
|
|||||||
'list_of_recurring_invoices' => 'List of recurring invoices',
|
'list_of_recurring_invoices' => 'List of recurring invoices',
|
||||||
'details_of_recurring_invoice' => 'Here are some details about recurring invoice',
|
'details_of_recurring_invoice' => 'Here are some details about recurring invoice',
|
||||||
'cancellation' => 'Cancellation',
|
'cancellation' => 'Cancellation',
|
||||||
'about_cancellation' => 'In case you want to stop the recurring invoice,\n please click the request the cancellation.',
|
'about_cancellation' => 'In case you want to stop the recurring invoice, please click the request the cancellation.',
|
||||||
'cancellation_warning' => 'Warning! You are requesting a cancellation of this service.\n Your service may be cancelled with no further notification to you.',
|
'cancellation_warning' => 'Warning! You are requesting a cancellation of this service.\n Your service may be cancelled with no further notification to you.',
|
||||||
'cancellation_pending' => 'Cancellation pending, we\'ll be in touch!',
|
'cancellation_pending' => 'Cancellation pending, we\'ll be in touch!',
|
||||||
'list_of_payments' => 'List of payments',
|
'list_of_payments' => 'List of payments',
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
@extends('portal.ninja2020.layout.clean')
|
@extends('portal.ninja2020.layout.clean')
|
||||||
@section('meta_title', $billing_subscription->product->product_key)
|
@section('meta_title', ctrans('texts.purchase'))
|
||||||
|
|
||||||
@section('body')
|
@section('body')
|
||||||
@livewire('billing-portal-purchase', ['billing_subscription' => $billing_subscription, 'contact' => auth('contact')->user(), 'hash' => $hash, 'request_data' => $request_data, 'price' => $billing_subscription->product->price])
|
@livewire('billing-portal-purchase', ['subscription' => $subscription, 'contact' => auth('contact')->user(), 'hash' => $hash, 'request_data' => $request_data])
|
||||||
@stop
|
@stop
|
||||||
|
|
||||||
@push('footer')
|
@push('footer')
|
||||||
|
16
resources/views/email/billing/passwordless-login.blade.php
Normal file
16
resources/views/email/billing/passwordless-login.blade.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@component('email.template.master', ['design' => 'light'])
|
||||||
|
@slot('header')
|
||||||
|
@include('email.components.header', ['logo' => 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png'])
|
||||||
|
@endslot
|
||||||
|
|
||||||
|
<h2>Login link requested</h2>
|
||||||
|
<p>Hey, there was a request to log in using link.</p>
|
||||||
|
|
||||||
|
<a href="{{ $url }}" target="_blank" class="button">Sign in to Invoice Ninja</a>
|
||||||
|
|
||||||
|
<span style="margin-top: 35px; display: block;">Link above is only for you. Don't share it anyone.</span>
|
||||||
|
<span>If you didn't request this, just ignore it.</span>
|
||||||
|
|
||||||
|
<span style="margin-top: 25px; display: block;">If you can't click on the button, copy following link:</span>
|
||||||
|
<a href="{{ $url }}">{{ $url }}</a>
|
||||||
|
@endcomponent
|
@ -1 +1 @@
|
|||||||
{{ $body }}
|
{!! $body !!}
|
@ -146,7 +146,13 @@
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script defer src="main.dart.js?v={{ config('ninja.app_version') }}" type="application/javascript"></script>
|
@if(config('ninja.flutter_canvas_kit') == 'hosted')
|
||||||
|
<script defer src="main.dart.js?v={{ config('ninja.app_version') }}" type="application/javascript"></script>
|
||||||
|
@elseif(config('ninja.flutter_canvas_kit') == 'selfhosted-canvaskit')
|
||||||
|
<script defer src="main.wasm.dart.js?v={{ config('ninja.app_version') }}" type="application/javascript"></script>
|
||||||
|
@else
|
||||||
|
<script defer src="main.foss.dart.js?v={{ config('ninja.app_version') }}" type="application/javascript"></script>
|
||||||
|
@endif
|
||||||
|
|
||||||
<center style="padding-top: 150px" id="loader">
|
<center style="padding-top: 150px" id="loader">
|
||||||
<div class="loader"></div>
|
<div class="loader"></div>
|
||||||
|
@ -1,48 +1,72 @@
|
|||||||
<div class="grid grid-cols-12">
|
<div class="grid grid-cols-12">
|
||||||
<div class="col-span-12 lg:col-span-6 bg-gray-50 shadow-lg lg:h-screen flex flex-col items-center">
|
<div class="col-span-12 lg:col-span-6 bg-gray-50 flex flex-col items-center">
|
||||||
<div class="w-full p-10 lg:w-1/2 lg:mt-48 lg:p-0">
|
<div class="w-full p-10 lg:w-1/2 lg:mt-24 lg:p-0">
|
||||||
<img class="h-8" src="{{ $billing_subscription->company->present()->logo }}"
|
<img class="h-8" src="{{ $subscription->company->present()->logo }}"
|
||||||
alt="{{ $billing_subscription->company->present()->name }}">
|
alt="{{ $subscription->company->present()->name }}">
|
||||||
|
|
||||||
<h1 id="billing-page-company-logo" class="text-3xl font-bold tracking-wide mt-8">
|
<div class="mt-6">
|
||||||
{{ $billing_subscription->product->product_key }}
|
<h1 id="billing-page-company-logo" class="text-3xl font-bold tracking-wide">
|
||||||
</h1>
|
{{ $subscription->name }}
|
||||||
|
</h1>
|
||||||
<p class="my-6">{{ $billing_subscription->product->notes }}</p>
|
|
||||||
|
|
||||||
<span class="text-sm uppercase font-bold">{{ ctrans('texts.price') }}:</span>
|
|
||||||
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<h1 class="text-2xl font-bold tracking-wide">{{ App\Utils\Number::formatMoney($price, $billing_subscription->company) }}</h1>
|
|
||||||
|
|
||||||
@if($billing_subscription->is_recurring)
|
|
||||||
<span class="text-xs uppercase">/ {{ \App\Models\RecurringInvoice::frequencyForKey($billing_subscription->frequency_id) }}</span>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($subscription->product_ids))
|
||||||
|
<div class="flex flex-col mt-8">
|
||||||
|
<p
|
||||||
|
class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
|
||||||
|
One-time purchases:
|
||||||
|
</p>
|
||||||
|
|
||||||
@if($billing_subscription->per_seat_enabled && $billing_subscription->max_seats_limit > 1)
|
@foreach($subscription->service()->products() as $product)
|
||||||
<div class="flex mt-4 space-x-4 items-center">
|
<div class="flex items-center justify-between mb-4 bg-white rounded px-6 py-4 shadow-sm border">
|
||||||
<span class="text-sm">{{ ctrans('texts.qty') }}</span>
|
<div class="text-sm">{{ $product->product_key }}</div>
|
||||||
<button wire:click="updateQuantity('decrement')" class="bg-gray-100 border rounded p-1">
|
<div data-ref="price-and-quantity-container">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
<span
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }}</span>
|
||||||
class="feather feather-minus">
|
{{-- <span data-ref="quantity" class="text-sm">(1x)</span>--}}
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
</div>
|
||||||
</svg>
|
</div>
|
||||||
</button>
|
@endforeach
|
||||||
<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>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if(!empty($subscription->recurring_product_ids))
|
||||||
|
<div class="flex flex-col mt-8">
|
||||||
|
<p
|
||||||
|
class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center rounded-full text-xs font-medium">
|
||||||
|
Recurring purchases:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@foreach($subscription->service()->recurring_products() as $product)
|
||||||
|
<div class="flex items-center justify-between mb-4 bg-white rounded px-6 py-4 shadow-sm border">
|
||||||
|
<div class="text-sm">{{ $product->product_key }}</div>
|
||||||
|
<div data-ref="price-and-quantity-container">
|
||||||
|
<span
|
||||||
|
data-ref="price">{{ \App\Utils\Number::formatMoney($product->price, $subscription->company) }}</span>
|
||||||
|
{{-- <span data-ref="quantity" class="text-sm">(1x)</span>--}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<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-50 px-6 py-0">
|
||||||
|
{{ ctrans('texts.total') }}
|
||||||
|
: {{ \App\Utils\Number::formatMoney($price, $subscription->company) }}
|
||||||
|
|
||||||
|
@if($steps['discount_applied'])
|
||||||
|
<small class="ml-1 line-through text-gray-500">{{ \App\Utils\Number::formatMoney($subscription->price, $subscription->company) }}</small>
|
||||||
|
@endif
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||||
@ -58,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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: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 ?? ctrans('texts.login') }}</h2>
|
<h2 class="text-2xl font-bold tracking-wide">{{ $heading_text ?? ctrans('texts.login') }}</h2>
|
||||||
@ -88,13 +112,25 @@
|
|||||||
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}"/>
|
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}"/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@foreach($this->methods as $method)
|
@if($steps['started_payment'] == false)
|
||||||
<button
|
@foreach($this->methods as $method)
|
||||||
wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')"
|
<button
|
||||||
class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
|
wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}')"
|
||||||
{{ $method['label'] }}
|
class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
|
||||||
</button>
|
{{ $method['label'] }}
|
||||||
@endforeach
|
</button>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($steps['started_payment'])
|
||||||
|
<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"
|
||||||
|
stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@elseif($steps['show_start_trial'])
|
@elseif($steps['show_start_trial'])
|
||||||
<form wire:submit.prevent="handleTrial" class="mt-8">
|
<form wire:submit.prevent="handleTrial" class="mt-8">
|
||||||
@ -133,6 +169,16 @@
|
|||||||
</p>
|
</p>
|
||||||
@enderror
|
@enderror
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<button wire:loading.attr="disabled" type="button" wire:click="passwordlessLogin"
|
||||||
|
class="mt-4 text-sm active:outline-none focus:outline-none">
|
||||||
|
Log in without password
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if($steps['passwordless_login_sent'])
|
||||||
|
<span
|
||||||
|
class="block mt-2 text-sm text-green-600">E-mail sent. Please check your inbox!</span>
|
||||||
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
@ -140,7 +186,7 @@
|
|||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if(!empty($billing_subscription->promo_code) && !$billing_subscription->trial_enabled)
|
@if(!empty($subscription->promo_code) && !$subscription->trial_enabled)
|
||||||
<div class="relative mt-8">
|
<div class="relative mt-8">
|
||||||
<div class="absolute inset-0 flex items-center">
|
<div class="absolute inset-0 flex items-center">
|
||||||
<div class="w-full border-t border-gray-300"></div>
|
<div class="w-full border-t border-gray-300"></div>
|
||||||
@ -151,12 +197,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mt-4">
|
<form wire:submit.prevent="handleCoupon" class="flex items-center mt-4">
|
||||||
|
@csrf
|
||||||
|
|
||||||
<label class="w-full mr-2">
|
<label class="w-full mr-2">
|
||||||
<input type="text" wire:model.lazy="coupon" class="input w-full m-0"/>
|
<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>
|
</label>
|
||||||
</div>
|
|
||||||
|
<button class="button button-primary bg-primary">Apply</button>
|
||||||
|
</form>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
<div>
|
||||||
|
<p class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary text-white">
|
||||||
|
One-time payments
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-2 text-sm hidden md:block">{{ ctrans('texts.per_page') }}</span>
|
||||||
|
<select wire:model="per_page" class="form-select py-1 text-sm">
|
||||||
|
<option>5</option>
|
||||||
|
<option selected>10</option>
|
||||||
|
<option>15</option>
|
||||||
|
<option>20</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||||
|
<div class="align-middle inline-block min-w-full overflow-hidden rounded">
|
||||||
|
<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.invoice') }}
|
||||||
|
</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">
|
||||||
|
<p role="button" wire:click="sortBy('amount')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.total') }}
|
||||||
|
</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">
|
||||||
|
<p role="button" wire:click="sortBy('date')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.date') }}
|
||||||
|
</p>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<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">
|
||||||
|
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}"
|
||||||
|
class="button-link text-primary">
|
||||||
|
{{ $invoice->number }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||||
|
{{ App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||||
|
{{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<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" colspan="100%">
|
||||||
|
{{ ctrans('texts.no_results') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center md:justify-between mt-6 mb-6">
|
||||||
|
@if($invoices->total() > 0)
|
||||||
|
<span class="text-gray-700 text-sm hidden md:block">
|
||||||
|
{{ ctrans('texts.showing_x_of', ['first' => $invoices->firstItem(), 'last' => $invoices->lastItem(), 'total' => $invoices->total()]) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
{{ $invoices->links('portal/ninja2020/vendor/pagination') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,75 @@
|
|||||||
|
<div>
|
||||||
|
<p class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary text-white">
|
||||||
|
Subscriptions
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-2 text-sm hidden md:block">{{ ctrans('texts.per_page') }}</span>
|
||||||
|
<select wire:model="per_page" class="form-select py-1 text-sm">
|
||||||
|
<option>5</option>
|
||||||
|
<option selected>10</option>
|
||||||
|
<option>15</option>
|
||||||
|
<option>20</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||||
|
<div class="align-middle inline-block min-w-full overflow-hidden rounded">
|
||||||
|
<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.invoice') }}
|
||||||
|
</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">
|
||||||
|
<p role="button" wire:click="sortBy('amount')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.total') }}
|
||||||
|
</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">
|
||||||
|
<p role="button" wire:click="sortBy('date')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.date') }}
|
||||||
|
</p>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<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">
|
||||||
|
<a href="{{ route('client.recurring_invoice.show', $recurring_invoice->hashed_id) }}"
|
||||||
|
class="button-link text-primary">
|
||||||
|
{{ $recurring_invoice->number }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||||
|
{{ App\Utils\Number::formatMoney($recurring_invoice->amount, $recurring_invoice->client) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||||
|
{{ $recurring_invoice->formatDate($recurring_invoice->date, $recurring_invoice->client->date_format()) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<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" colspan="100%">
|
||||||
|
{{ ctrans('texts.no_results') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center md:justify-between mt-6 mb-6">
|
||||||
|
@if($recurring_invoices->total() > 0)
|
||||||
|
<span class="text-gray-700 text-sm hidden md:block">
|
||||||
|
{{ ctrans('texts.showing_x_of', ['first' => $recurring_invoices->firstItem(), 'last' => $recurring_invoices->lastItem(), 'total' => $recurring_invoices->total()]) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
{{ $recurring_invoices->links('portal/ninja2020/vendor/pagination') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,150 @@
|
|||||||
|
<div>
|
||||||
|
<p class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary text-white"
|
||||||
|
translate="yes">
|
||||||
|
One-time payments
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-2 text-sm hidden md:block">{{ ctrans('texts.per_page') }}</span>
|
||||||
|
<select wire:model="per_page" class="form-select py-1 text-sm">
|
||||||
|
<option>5</option>
|
||||||
|
<option selected>10</option>
|
||||||
|
<option>15</option>
|
||||||
|
<option>20</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||||
|
<div class="align-middle inline-block min-w-full overflow-hidden rounded">
|
||||||
|
<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">
|
||||||
|
<span role="button" wire:click="sortBy('number')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.invoice') }}
|
||||||
|
</span>
|
||||||
|
</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">
|
||||||
|
<span role="button" wire:click="sortBy('amount')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.total') }}
|
||||||
|
</span>
|
||||||
|
</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">
|
||||||
|
<span role="button" wire:click="sortBy('public_notes')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.date') }}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<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">
|
||||||
|
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}"
|
||||||
|
class="button-link text-primary">
|
||||||
|
{{ $invoice->number }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||||
|
{{ App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||||
|
{{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<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" colspan="100%">
|
||||||
|
{{ ctrans('texts.no_results') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center md:justify-between mt-6 mb-6">
|
||||||
|
@if($invoices->total() > 0)
|
||||||
|
<span class="text-gray-700 text-sm hidden md:block">
|
||||||
|
{{ ctrans('texts.showing_x_of', ['first' => $invoices->firstItem(), 'last' => $invoices->lastItem(), 'total' => $invoices->total()]) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
{{ $invoices->links('portal/ninja2020/vendor/pagination') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary text-white mt-4"
|
||||||
|
translate="yes">
|
||||||
|
Subscriptions
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-2 text-sm hidden md:block">{{ ctrans('texts.per_page') }}</span>
|
||||||
|
<select wire:model="per_page" class="form-select py-1 text-sm">
|
||||||
|
<option>5</option>
|
||||||
|
<option selected>10</option>
|
||||||
|
<option>15</option>
|
||||||
|
<option>20</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||||
|
<div class="align-middle inline-block min-w-full overflow-hidden rounded">
|
||||||
|
<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">
|
||||||
|
<span role="button" wire:click="sortBy('number')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.invoice') }}
|
||||||
|
</span>
|
||||||
|
</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">
|
||||||
|
<span role="button" wire:click="sortBy('amount')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.total') }}
|
||||||
|
</span>
|
||||||
|
</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">
|
||||||
|
<span role="button" wire:click="sortBy('public_notes')" class="cursor-pointer">
|
||||||
|
{{ ctrans('texts.date') }}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<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">
|
||||||
|
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}"
|
||||||
|
class="button-link text-primary">
|
||||||
|
{{ $invoice->number }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||||
|
{{ App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||||
|
{{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<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" colspan="100%">
|
||||||
|
{{ ctrans('texts.no_results') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center md:justify-between mt-6 mb-6">
|
||||||
|
@if($invoices->total() > 0)
|
||||||
|
<span class="text-gray-700 text-sm hidden md:block">
|
||||||
|
{{ ctrans('texts.showing_x_of', ['first' => $invoices->firstItem(), 'last' => $invoices->lastItem(), 'total' => $invoices->total()]) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
{{ $invoices->links('portal/ninja2020/vendor/pagination') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@ -8,7 +8,7 @@
|
|||||||
<div class="px-4 py-5 sm:p-6">
|
<div class="px-4 py-5 sm:p-6">
|
||||||
<div class="sm:flex sm:items-start sm:justify-between">
|
<div class="sm:flex sm:items-start sm:justify-between">
|
||||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
{{ ctrans('texts.pending_approval') }}
|
{{ ctrans('texts.approve') }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
|
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
|
||||||
|
@ -55,29 +55,35 @@
|
|||||||
{{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
|
{{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white shadow sm:rounded-lg mb-4 mt-4" translate>
|
|
||||||
<div class="px-4 py-5 sm:p-6">
|
@if(is_null($invoice->subscription_id) || optional($invoice->subscription)->allow_cancellation)
|
||||||
<div class="sm:flex sm:items-start sm:justify-between">
|
<div class="bg-white shadow sm:rounded-lg mb-4 mt-4">
|
||||||
<div>
|
<div class="px-4 py-5 sm:p-6">
|
||||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
<div class="sm:flex sm:items-start sm:justify-between">
|
||||||
{{ ctrans('texts.cancellation') }}
|
<div>
|
||||||
</h3>
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
|
{{ ctrans('texts.cancellation') }}
|
||||||
<p translate>
|
</h3>
|
||||||
{{ ctrans('texts.about_cancellation') }}
|
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
|
||||||
</p>
|
<p translate>
|
||||||
|
{{ ctrans('texts.about_cancellation') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
|
||||||
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
|
<div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }">
|
||||||
<div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }">
|
<button class="button button-danger" translate @click="open = true">Request Cancellation
|
||||||
<button class="button button-danger" translate @click="open = true">Request Cancellation</button>
|
</button>
|
||||||
@include('portal.ninja2020.recurring_invoices.includes.modals.cancellation')
|
@include('portal.ninja2020.recurring_invoices.includes.modals.cancellation')
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
@extends('portal.ninja2020.layout.app')
|
||||||
|
@section('meta_title', ctrans('texts.subscriptions'))
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<div class="flex flex-col">
|
||||||
|
@livewire('subscription-invoices-table')
|
||||||
|
@livewire('subscription-recurring-invoices-table')
|
||||||
|
</div>
|
||||||
|
@endsection
|
@ -22,6 +22,7 @@ Route::post('view/{entity_type}/{invitation_key}/password', 'ClientPortal\Entity
|
|||||||
Route::get('tmp_pdf/{hash}', 'ClientPortal\TempRouteController@index')->name('tmp_pdf');
|
Route::get('tmp_pdf/{hash}', 'ClientPortal\TempRouteController@index')->name('tmp_pdf');
|
||||||
|
|
||||||
Route::get('client/key_login/{contact_key}', 'ClientPortal\ContactHashLoginController@login')->name('client.contact_login')->middleware(['contact_key_login']);
|
Route::get('client/key_login/{contact_key}', 'ClientPortal\ContactHashLoginController@login')->name('client.contact_login')->middleware(['contact_key_login']);
|
||||||
|
Route::get('client/magic_link/{magic_link}', 'ClientPortal\ContactHashLoginController@magicLink')->name('client.contact_magic_link')->middleware(['contact_key_login']);
|
||||||
Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download');
|
Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download');
|
||||||
|
|
||||||
//todo implement domain DB
|
//todo implement domain DB
|
||||||
@ -71,12 +72,14 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
|
|||||||
Route::get('documents/{document}/download', 'ClientPortal\DocumentController@download')->name('documents.download');
|
Route::get('documents/{document}/download', 'ClientPortal\DocumentController@download')->name('documents.download');
|
||||||
Route::resource('documents', 'ClientPortal\DocumentController')->only(['index', 'show']);
|
Route::resource('documents', 'ClientPortal\DocumentController')->only(['index', 'show']);
|
||||||
|
|
||||||
|
Route::resource('subscriptions', 'ClientPortal\SubscriptionController')->only(['index']);
|
||||||
|
|
||||||
Route::post('upload', 'ClientPortal\UploadController')->name('upload.store');
|
Route::post('upload', 'ClientPortal\UploadController')->name('upload.store');
|
||||||
|
|
||||||
Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
|
Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('client/subscription/{billing_subscription}/purchase', 'ClientPortal\BillingSubscriptionPurchaseController@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 () {
|
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
|
||||||
/*Invitation catches*/
|
/*Invitation catches*/
|
||||||
|
3
tailwind.config.js
vendored
3
tailwind.config.js
vendored
@ -1,6 +1,9 @@
|
|||||||
const defaultTheme = require("tailwindcss/defaultTheme");
|
const defaultTheme = require("tailwindcss/defaultTheme");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
future: {
|
||||||
|
purgeLayersByDefault: true
|
||||||
|
},
|
||||||
purge: [
|
purge: [
|
||||||
'./resources/views/portal/ninja2020/**/*.blade.php',
|
'./resources/views/portal/ninja2020/**/*.blade.php',
|
||||||
'./resources/views/email/template/**/*.blade.php',
|
'./resources/views/email/template/**/*.blade.php',
|
||||||
|
@ -11,14 +11,15 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\Subscription;
|
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
|
use App\Models\Subscription;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Foundation\Testing\WithFaker;
|
use Illuminate\Foundation\Testing\WithFaker;
|
||||||
use Illuminate\Support\Facades\Session;
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Tests\MockAccountData;
|
use Tests\MockAccountData;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@ -55,6 +56,7 @@ class SubscriptionApiTest extends TestCase
|
|||||||
$billing_subscription = Subscription::factory()->create([
|
$billing_subscription = Subscription::factory()->create([
|
||||||
'product_ids' => $product->id,
|
'product_ids' => $product->id,
|
||||||
'company_id' => $this->company->id,
|
'company_id' => $this->company->id,
|
||||||
|
'name' => Str::random(5)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
@ -78,8 +80,9 @@ class SubscriptionApiTest extends TestCase
|
|||||||
$response = $this->withHeaders([
|
$response = $this->withHeaders([
|
||||||
'X-API-SECRET' => config('ninja.api_secret'),
|
'X-API-SECRET' => config('ninja.api_secret'),
|
||||||
'X-API-TOKEN' => $this->token,
|
'X-API-TOKEN' => $this->token,
|
||||||
])->post('/api/v1/subscriptions', ['product_ids' => $product->id, 'allow_cancellation' => true]);
|
])->post('/api/v1/subscriptions', ['product_ids' => $product->id, 'allow_cancellation' => true, 'name' => Str::random(5)]);
|
||||||
|
|
||||||
|
// nlog($response);
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +95,7 @@ class SubscriptionApiTest extends TestCase
|
|||||||
|
|
||||||
$response1 = $this
|
$response1 = $this
|
||||||
->withHeaders(['X-API-SECRET' => config('ninja.api_secret'),'X-API-TOKEN' => $this->token])
|
->withHeaders(['X-API-SECRET' => config('ninja.api_secret'),'X-API-TOKEN' => $this->token])
|
||||||
->post('/api/v1/subscriptions', ['product_ids' => $product->id])
|
->post('/api/v1/subscriptions', ['product_ids' => $product->id, 'name' => Str::random(5)])
|
||||||
->assertStatus(200)
|
->assertStatus(200)
|
||||||
->json();
|
->json();
|
||||||
|
|
||||||
@ -125,6 +128,7 @@ class SubscriptionApiTest extends TestCase
|
|||||||
$billing_subscription = Subscription::factory()->create([
|
$billing_subscription = Subscription::factory()->create([
|
||||||
'product_ids' => $product->id,
|
'product_ids' => $product->id,
|
||||||
'company_id' => $this->company->id,
|
'company_id' => $this->company->id,
|
||||||
|
'name' => Str::random(5)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
|
@ -310,6 +310,28 @@ class AuthorizeTest extends TestCase
|
|||||||
$controller = new CreateTransactionController($request);
|
$controller = new CreateTransactionController($request);
|
||||||
$response = $controller->executeWithApiResponse(\net\authorize\api\constants\ANetEnvironment::SANDBOX);
|
$response = $controller->executeWithApiResponse(\net\authorize\api\constants\ANetEnvironment::SANDBOX);
|
||||||
|
|
||||||
|
// nlog($response);
|
||||||
|
nlog($response->getTransactionResponse()->getMessages() !== null);
|
||||||
|
nlog($response->getTransactionResponse()->getMessages());
|
||||||
|
nlog($response->getTransactionResponse()->getMessages()[0]);
|
||||||
|
//nlog($response->getTransactionResponse()->getMessages()[0]->getCode());
|
||||||
|
|
||||||
|
$code = '';
|
||||||
|
$description = '';
|
||||||
|
|
||||||
|
if($response->getTransactionResponse()->getMessages() !== null){
|
||||||
|
$code = $response->getTransactionResponse()->getMessages()[0]->getCode();
|
||||||
|
$description = $response->getTransactionResponse()->getMessages()[0]->getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
$log = [
|
||||||
|
'transaction_reference' => $response->getTransactionResponse()->getTransId(),
|
||||||
|
'auth_code' => $response->getTransactionResponse()->getAuthCode(),
|
||||||
|
'code' => $code,
|
||||||
|
'description' => $description,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
if ($response != null) {
|
if ($response != null) {
|
||||||
if ($response->getMessages()->getResultCode() == 'Ok') {
|
if ($response->getMessages()->getResultCode() == 'Ok') {
|
||||||
$tresponse = $response->getTransactionResponse();
|
$tresponse = $response->getTransactionResponse();
|
||||||
|
44
tests/Unit/RecurringDateTest.php
Normal file
44
tests/Unit/RecurringDateTest.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?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 Tests\Unit;
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Tests\MockAccountData;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
class RecurringDateTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
use MockAccountData;
|
||||||
|
|
||||||
|
public function setUp() :void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
//$this->makeTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNextDay()
|
||||||
|
{
|
||||||
|
$trial = 60*60*24;
|
||||||
|
|
||||||
|
$now = Carbon::parse('2021-12-01');
|
||||||
|
|
||||||
|
$trial_ends = $now->addSeconds($trial)->addDays(1);
|
||||||
|
|
||||||
|
$this->assertequals($trial_ends->format('Y-m-d'), '2021-12-03');
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user