mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Merge branch 'v5-develop' of https://github.com/invoiceninja/invoiceninja into feature-inbound-email-expenses
This commit is contained in:
commit
9a733f06c0
3
.github/workflows/react_release.yml
vendored
3
.github/workflows/react_release.yml
vendored
@ -49,7 +49,8 @@ jobs:
|
|||||||
npm i
|
npm i
|
||||||
npm run build
|
npm run build
|
||||||
cp -r dist/* ../public/
|
cp -r dist/* ../public/
|
||||||
|
cp dist/index.html ../resources/views/react/index.blade.php
|
||||||
|
|
||||||
- name: Prepare JS/CSS assets
|
- name: Prepare JS/CSS assets
|
||||||
run: |
|
run: |
|
||||||
npm i
|
npm i
|
||||||
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -50,7 +50,8 @@ jobs:
|
|||||||
mkdir -p ../public/react/${{ github.event.release.tag_name }}/
|
mkdir -p ../public/react/${{ github.event.release.tag_name }}/
|
||||||
cp -r dist/react/* ../public/react/${{ github.event.release.tag_name }}/
|
cp -r dist/react/* ../public/react/${{ github.event.release.tag_name }}/
|
||||||
cp -r dist/react/* ../public/react/
|
cp -r dist/react/* ../public/react/
|
||||||
|
cp dist/index.html ../resources/views/react/index.blade.php
|
||||||
|
|
||||||
mkdir -p ../public/tinymce_6.4.2/tinymce/js/
|
mkdir -p ../public/tinymce_6.4.2/tinymce/js/
|
||||||
cp -r node_modules/tinymce ../public/tinymce_6.4.2/tinymce/js/
|
cp -r node_modules/tinymce ../public/tinymce_6.4.2/tinymce/js/
|
||||||
cd ..
|
cd ..
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -39,4 +39,5 @@ public/storage/test.pdf
|
|||||||
_ide_helper_models.php
|
_ide_helper_models.php
|
||||||
_ide_helper.php
|
_ide_helper.php
|
||||||
/composer.phar
|
/composer.phar
|
||||||
.tx/
|
.tx/
|
||||||
|
.phpunit.cache
|
@ -1 +1 @@
|
|||||||
5.8.39
|
5.8.45
|
@ -42,35 +42,35 @@ class ReactBuilder extends Command
|
|||||||
/**
|
/**
|
||||||
* Execute the console command.
|
* Execute the console command.
|
||||||
*
|
*
|
||||||
* @return mixed
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$includes = '';
|
// $includes = '';
|
||||||
|
|
||||||
$directoryIterator = false;
|
// $directoryIterator = false;
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
$directoryIterator = new \RecursiveDirectoryIterator(public_path('react/v'.config('ninja.app_version').'/'), \RecursiveDirectoryIterator::SKIP_DOTS);
|
// $directoryIterator = new \RecursiveDirectoryIterator(public_path('react/v'.config('ninja.app_version').'/'), \RecursiveDirectoryIterator::SKIP_DOTS);
|
||||||
} catch (\Exception $e) {
|
// } catch (\Exception $e) {
|
||||||
$this->error('React files not found');
|
// $this->error('React files not found');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) {
|
// foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) {
|
||||||
if ($file->getExtension() == 'js') {
|
// if ($file->getExtension() == 'js') {
|
||||||
if (str_contains($file->getFileName(), 'index-')) {
|
// if (str_contains($file->getFileName(), 'index-')) {
|
||||||
$includes .= '<script type="module" crossorigin src="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'"></script>'."\n";
|
// $includes .= '<script type="module" crossorigin src="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'"></script>'."\n";
|
||||||
} else {
|
// } else {
|
||||||
$includes .= '<link rel="modulepreload" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
|
// $includes .= '<link rel="modulepreload" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (str_contains($file->getFileName(), '.css')) {
|
// if (str_contains($file->getFileName(), '.css')) {
|
||||||
$includes .= '<link rel="stylesheet" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
|
// $includes .= '<link rel="stylesheet" href="/react/v'.config('ninja.app_version').'/'.$file->getFileName().'">'."\n";
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
file_put_contents(resource_path('views/react/head.blade.php'), $includes);
|
// file_put_contents(resource_path('views/react/head.blade.php'), $includes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,9 @@ class InvoiceItem
|
|||||||
public $task_id = '';
|
public $task_id = '';
|
||||||
|
|
||||||
public $expense_id = '';
|
public $expense_id = '';
|
||||||
|
|
||||||
|
public $unit_code = 'C62';
|
||||||
|
|
||||||
public static $casts = [
|
public static $casts = [
|
||||||
'task_id' => 'string',
|
'task_id' => 'string',
|
||||||
'expense_id' => 'string',
|
'expense_id' => 'string',
|
||||||
@ -92,5 +94,6 @@ class InvoiceItem
|
|||||||
'custom_value2' => 'string',
|
'custom_value2' => 'string',
|
||||||
'custom_value3' => 'string',
|
'custom_value3' => 'string',
|
||||||
'custom_value4' => 'string',
|
'custom_value4' => 'string',
|
||||||
|
'unit_code' => 'string',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
namespace App\Factory;
|
namespace App\Factory;
|
||||||
|
|
||||||
|
use App\Livewire\BillingPortal\Purchase;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
|
use App\Services\Subscription\StepService;
|
||||||
|
|
||||||
class SubscriptionFactory
|
class SubscriptionFactory
|
||||||
{
|
{
|
||||||
@ -20,6 +22,9 @@ class SubscriptionFactory
|
|||||||
$billing_subscription = new Subscription();
|
$billing_subscription = new Subscription();
|
||||||
$billing_subscription->company_id = $company_id;
|
$billing_subscription->company_id = $company_id;
|
||||||
$billing_subscription->user_id = $user_id;
|
$billing_subscription->user_id = $user_id;
|
||||||
|
$billing_subscription->steps = collect(Purchase::defaultSteps())
|
||||||
|
->map(fn($step) => StepService::mapClassNameToString($step))
|
||||||
|
->implode(',');
|
||||||
|
|
||||||
return $billing_subscription;
|
return $billing_subscription;
|
||||||
}
|
}
|
||||||
|
@ -150,6 +150,19 @@ class TaskFilters extends QueryFilters
|
|||||||
return $this->builder->orderBy($sort_col[0], $dir);
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function user_id(string $user = ''): Builder
|
||||||
|
{
|
||||||
|
if (strlen($user) == 0) {
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->where(function ($query) use ($user) {
|
||||||
|
$query->where('user_id', $this->decodePrimaryKey($user))
|
||||||
|
->orWhere('assigned_user_id', $this->decodePrimaryKey($user));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function task_status(string $value = ''): Builder
|
public function task_status(string $value = ''): Builder
|
||||||
{
|
{
|
||||||
if (strlen($value) == 0) {
|
if (strlen($value) == 0) {
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Invoice Ninja (https://invoiceninja.com).
|
|
||||||
*
|
|
||||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
|
||||||
*
|
|
||||||
* @license https://www.elastic.co/licensing/elastic-license
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Helpers\Subscription;
|
|
||||||
|
|
||||||
use App\Helpers\Invoice\ProRata;
|
|
||||||
use App\Models\Invoice;
|
|
||||||
use App\Models\Subscription;
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SubscriptionCalculator.
|
|
||||||
*/
|
|
||||||
class SubscriptionCalculator
|
|
||||||
{
|
|
||||||
public Subscription $target_subscription;
|
|
||||||
|
|
||||||
public Invoice $invoice;
|
|
||||||
|
|
||||||
public function __construct(Subscription $target_subscription, Invoice $invoice)
|
|
||||||
{
|
|
||||||
$this->target_subscription = $target_subscription;
|
|
||||||
$this->invoice = $invoice;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests if the user is currently up
|
|
||||||
* to date with their payments for
|
|
||||||
* a given recurring invoice
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isPaidUp(): bool
|
|
||||||
{
|
|
||||||
$outstanding_invoices_exist = Invoice::query()->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
|
||||||
->where('subscription_id', $this->invoice->subscription_id)
|
|
||||||
->where('client_id', $this->invoice->client_id)
|
|
||||||
->where('balance', '>', 0)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
return ! $outstanding_invoices_exist;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function calcUpgradePlan()
|
|
||||||
{
|
|
||||||
//set the starting refund amount
|
|
||||||
$refund_amount = 0;
|
|
||||||
|
|
||||||
$refund_invoice = false;
|
|
||||||
|
|
||||||
//are they paid up to date.
|
|
||||||
|
|
||||||
//yes - calculate refund
|
|
||||||
if ($this->isPaidUp()) {
|
|
||||||
$refund_invoice = $this->getRefundInvoice();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($refund_invoice) {
|
|
||||||
/** @var \App\Models\Subscription $subscription **/
|
|
||||||
$subscription = Subscription::find($this->invoice->subscription_id);
|
|
||||||
$pro_rata = new ProRata();
|
|
||||||
|
|
||||||
$to_date = $subscription->service()->getNextDateForFrequency(Carbon::parse($refund_invoice->date), $subscription->frequency_id);
|
|
||||||
|
|
||||||
$refund_amount = $pro_rata->refund($refund_invoice->amount, now(), $to_date, $subscription->frequency_id);
|
|
||||||
|
|
||||||
$charge_amount = $pro_rata->charge($this->target_subscription->price, now(), $to_date, $this->target_subscription->frequency_id);
|
|
||||||
|
|
||||||
return $charge_amount - $refund_amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
//no - return full freight charge.
|
|
||||||
return $this->target_subscription->price;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function executeUpgradePlan()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getRefundInvoice()
|
|
||||||
{
|
|
||||||
return Invoice::where('subscription_id', $this->invoice->subscription_id)
|
|
||||||
->where('client_id', $this->invoice->client_id)
|
|
||||||
->where('is_deleted', 0)
|
|
||||||
->orderBy('id', 'desc')
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,17 +11,14 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Factory\ClientContactFactory;
|
|
||||||
use App\Factory\ClientFactory;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\ClientPortal\RegisterRequest;
|
use App\Http\Requests\ClientPortal\RegisterRequest;
|
||||||
use App\Models\Client;
|
use App\Livewire\BillingPortal\Authentication\ClientRegisterService;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
use App\Utils\Traits\GeneratesCounter;
|
use App\Utils\Traits\GeneratesCounter;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
|
|
||||||
class ContactRegisterController extends Controller
|
class ContactRegisterController extends Controller
|
||||||
{
|
{
|
||||||
@ -53,53 +50,16 @@ class ContactRegisterController extends Controller
|
|||||||
public function register(RegisterRequest $request)
|
public function register(RegisterRequest $request)
|
||||||
{
|
{
|
||||||
$request->merge(['company' => $request->company()]);
|
$request->merge(['company' => $request->company()]);
|
||||||
|
|
||||||
|
$service = new ClientRegisterService(
|
||||||
|
company: $request->company(),
|
||||||
|
);
|
||||||
|
|
||||||
$client = $this->getClient($request->all());
|
$client = $service->createClient($request->all());
|
||||||
$client_contact = $this->getClientContact($request->all(), $client);
|
$client_contact = $service->createClientContact($request->all(), $client);
|
||||||
|
|
||||||
Auth::guard('contact')->loginUsingId($client_contact->id, true);
|
Auth::guard('contact')->loginUsingId($client_contact->id, true);
|
||||||
|
|
||||||
return redirect()->intended(route('client.dashboard'));
|
return redirect()->intended(route('client.dashboard'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getClient(array $data)
|
|
||||||
{
|
|
||||||
$client = ClientFactory::create($data['company']->id, $data['company']->owner()->id);
|
|
||||||
|
|
||||||
$client->fill($data);
|
|
||||||
|
|
||||||
$client->save();
|
|
||||||
|
|
||||||
if (isset($data['currency_id'])) {
|
|
||||||
$settings = $client->settings;
|
|
||||||
$settings->currency_id = isset($data['currency_id']) ? $data['currency_id'] : $data['company']->settings->currency_id;
|
|
||||||
$client->settings = $settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
$client->number = $this->getNextClientNumber($client);
|
|
||||||
$client->save();
|
|
||||||
|
|
||||||
if (! array_key_exists('country_id', $data) && strlen($client->company->settings->country_id) > 1) {
|
|
||||||
$client->update(['country_id' => $client->company->settings->country_id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClientContact(array $data, Client $client)
|
|
||||||
{
|
|
||||||
$client_contact = ClientContactFactory::create($data['company']->id, $data['company']->owner()->id);
|
|
||||||
$client_contact->fill($data);
|
|
||||||
|
|
||||||
$client_contact->client_id = $client->id;
|
|
||||||
$client_contact->is_primary = true;
|
|
||||||
|
|
||||||
if (array_key_exists('password', $data)) {
|
|
||||||
$client_contact->password = Hash::make($data['password']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$client_contact->save();
|
|
||||||
|
|
||||||
return $client_contact;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -237,7 +237,7 @@ class ClientController extends BaseController
|
|||||||
$hash_or_response = $request->boolean('send_email') ? 'email sent' : \Illuminate\Support\Str::uuid();
|
$hash_or_response = $request->boolean('send_email') ? 'email sent' : \Illuminate\Support\Str::uuid();
|
||||||
|
|
||||||
TemplateAction::dispatch(
|
TemplateAction::dispatch(
|
||||||
$clients->pluck('id')->toArray(),
|
$clients->pluck('hashed_id')->toArray(),
|
||||||
$request->template_id,
|
$request->template_id,
|
||||||
Client::class,
|
Client::class,
|
||||||
$user->id,
|
$user->id,
|
||||||
@ -250,6 +250,14 @@ class ClientController extends BaseController
|
|||||||
return response()->json(['message' => $hash_or_response], 200);
|
return response()->json(['message' => $hash_or_response], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($action == 'assign_group' && $user->can('edit', $clients->first())){
|
||||||
|
|
||||||
|
$this->client_repo->assignGroup($clients, $request->group_settings_id);
|
||||||
|
|
||||||
|
return $this->listResponse(Client::query()->withTrashed()->company()->whereIn('id', $request->ids));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
$clients->each(function ($client) use ($action, $user) {
|
$clients->each(function ($client) use ($action, $user) {
|
||||||
if ($user->can('edit', $client)) {
|
if ($user->can('edit', $client)) {
|
||||||
$this->client_repo->{$action}($client);
|
$this->client_repo->{$action}($client);
|
||||||
|
@ -105,6 +105,13 @@ class PrePaymentController extends Controller
|
|||||||
return $invoice;
|
return $invoice;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$variables = false;
|
||||||
|
|
||||||
|
if(($invitation = $invoices->first()->invitations()->first() ?? false) && $invoice->client->getSetting('show_accept_invoice_terms')) {
|
||||||
|
$variables = (new HtmlEngine($invitation))->generateLabelsAndValues();
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'settings' => auth()->guard('contact')->user()->client->getMergedSettings(),
|
'settings' => auth()->guard('contact')->user()->client->getMergedSettings(),
|
||||||
'invoices' => $invoices,
|
'invoices' => $invoices,
|
||||||
|
@ -75,6 +75,17 @@ class SubscriptionPurchaseController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function v3(Subscription $subscription, Request $request)
|
||||||
|
{
|
||||||
|
// Todo: Prerequirement checks for subscription.
|
||||||
|
|
||||||
|
return view('billing-portal.v3.index', [
|
||||||
|
'subscription' => $subscription,
|
||||||
|
'hash' => Str::uuid()->toString(),
|
||||||
|
'request_data' => $request->all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set locale for incoming request.
|
* Set locale for incoming request.
|
||||||
*
|
*
|
||||||
|
@ -565,9 +565,11 @@ class CompanyGatewayController extends BaseController
|
|||||||
|
|
||||||
public function importCustomers(TestCompanyGatewayRequest $request, CompanyGateway $company_gateway)
|
public function importCustomers(TestCompanyGatewayRequest $request, CompanyGateway $company_gateway)
|
||||||
{
|
{
|
||||||
|
// $x = Cache::pull("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}");
|
||||||
|
|
||||||
//Throttle here
|
//Throttle here
|
||||||
// if (Cache::get("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}"))
|
if (Cache::has("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}"))
|
||||||
// return response()->json(['message' => ctrans('texts.import_started')], 200);
|
return response()->json(['message' => 'Please wait whilst your previous attempts complete.'], 200);
|
||||||
|
|
||||||
dispatch(function () use($company_gateway) {
|
dispatch(function () use($company_gateway) {
|
||||||
MultiDB::setDb($company_gateway->company->db);
|
MultiDB::setDb($company_gateway->company->db);
|
||||||
|
@ -781,7 +781,7 @@ class CreditController extends BaseController
|
|||||||
$contact = $invitation->contact;
|
$contact = $invitation->contact;
|
||||||
$credit = $invitation->credit;
|
$credit = $invitation->credit;
|
||||||
|
|
||||||
$file = $credit->service()->getEInvoice($contact);
|
$file = $credit->service()->getECredit($contact);
|
||||||
$file_name = $credit->getFileName("xml");
|
$file_name = $credit->getFileName("xml");
|
||||||
|
|
||||||
$headers = ['Content-Type' => 'application/xml'];
|
$headers = ['Content-Type' => 'application/xml'];
|
||||||
|
@ -646,7 +646,6 @@ class PurchaseOrderController extends BaseController
|
|||||||
echo $file;
|
echo $file;
|
||||||
}, $purchase_order->numberFormatter().".pdf", ['Content-Type' => 'application/pdf']);
|
}, $purchase_order->numberFormatter().".pdf", ['Content-Type' => 'application/pdf']);
|
||||||
|
|
||||||
break;
|
|
||||||
case 'restore':
|
case 'restore':
|
||||||
$this->purchase_order_repository->restore($purchase_order);
|
$this->purchase_order_repository->restore($purchase_order);
|
||||||
|
|
||||||
|
43
app/Http/Controllers/SubscriptionStepsController.php
Normal file
43
app/Http/Controllers/SubscriptionStepsController.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Livewire\BillingPortal\Purchase;
|
||||||
|
use App\Rules\Subscriptions\Steps;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class SubscriptionStepsController extends BaseController
|
||||||
|
{
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$dependencies = collect(Purchase::$dependencies)
|
||||||
|
->map(fn($dependency) => [
|
||||||
|
'id' => $dependency['id'],
|
||||||
|
'dependencies' => collect($dependency['dependencies'])
|
||||||
|
->map(fn($dependency) => Purchase::$dependencies[$dependency]['id'])
|
||||||
|
->toArray(),
|
||||||
|
])
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return response()->json($dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check(): JsonResponse
|
||||||
|
{
|
||||||
|
request()->validate(([
|
||||||
|
'steps' => ['required', new Steps()]
|
||||||
|
]));
|
||||||
|
|
||||||
|
return response()->json([], 200);
|
||||||
|
}
|
||||||
|
}
|
@ -35,10 +35,11 @@ class BulkClientRequest extends Request
|
|||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'action' => 'required|string|in:archive,restore,delete,template',
|
'action' => 'required|string|in:archive,restore,delete,template,assign_group',
|
||||||
'ids' => ['required','bail','array',Rule::exists('clients', 'id')->where('company_id', $user->company()->id)],
|
'ids' => ['required','bail','array',Rule::exists('clients', 'id')->where('company_id', $user->company()->id)],
|
||||||
'template' => 'sometimes|string',
|
'template' => 'sometimes|string',
|
||||||
'template_id' => 'sometimes|string',
|
'template_id' => 'sometimes|string',
|
||||||
|
'group_settings_id' => ['required_if:action,assign_group',Rule::exists('group_settings', 'id')->where('company_id', $user->company()->id)],
|
||||||
'send_email' => 'sometimes|bool'
|
'send_email' => 'sometimes|bool'
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -52,6 +53,10 @@ class BulkClientRequest extends Request
|
|||||||
$input['ids'] = $this->transformKeys($input['ids']);
|
$input['ids'] = $this->transformKeys($input['ids']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($input['group_settings_id'])) {
|
||||||
|
$input['group_settings_id'] = $this->decodePrimaryKey($input['group_settings_id']);
|
||||||
|
}
|
||||||
|
|
||||||
$this->replace($input);
|
$this->replace($input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ namespace App\Http\Requests\Subscription;
|
|||||||
use App\Http\Requests\Request;
|
use App\Http\Requests\Request;
|
||||||
use App\Models\Account;
|
use App\Models\Account;
|
||||||
use App\Models\Subscription;
|
use App\Models\Subscription;
|
||||||
|
use App\Rules\Subscriptions\Steps;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class StoreSubscriptionRequest extends Request
|
class StoreSubscriptionRequest extends Request
|
||||||
@ -63,7 +64,8 @@ class StoreSubscriptionRequest extends Request
|
|||||||
'registration_required' => 'bail|sometimes|bool',
|
'registration_required' => 'bail|sometimes|bool',
|
||||||
'optional_recurring_product_ids' => 'bail|sometimes|nullable|string',
|
'optional_recurring_product_ids' => 'bail|sometimes|nullable|string',
|
||||||
'optional_product_ids' => 'bail|sometimes|nullable|string',
|
'optional_product_ids' => 'bail|sometimes|nullable|string',
|
||||||
'use_inventory_management' => 'bail|sometimes|bool'
|
'use_inventory_management' => 'bail|sometimes|bool',
|
||||||
|
'steps' => ['required', new Steps()],
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->globalRules($rules);
|
return $this->globalRules($rules);
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace App\Http\Requests\Subscription;
|
namespace App\Http\Requests\Subscription;
|
||||||
|
|
||||||
use App\Http\Requests\Request;
|
use App\Http\Requests\Request;
|
||||||
|
use App\Rules\Subscriptions\Steps;
|
||||||
use App\Utils\Traits\ChecksEntityStatus;
|
use App\Utils\Traits\ChecksEntityStatus;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ class UpdateSubscriptionRequest extends Request
|
|||||||
'optional_recurring_product_ids' => 'bail|sometimes|nullable|string',
|
'optional_recurring_product_ids' => 'bail|sometimes|nullable|string',
|
||||||
'optional_product_ids' => 'bail|sometimes|nullable|string',
|
'optional_product_ids' => 'bail|sometimes|nullable|string',
|
||||||
'use_inventory_management' => 'bail|sometimes|bool',
|
'use_inventory_management' => 'bail|sometimes|bool',
|
||||||
|
'steps' => ['required', new Steps()],
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->globalRules($rules);
|
return $this->globalRules($rules);
|
||||||
|
@ -39,62 +39,25 @@ class SubscriptionCron
|
|||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
nlog('Subscription Cron');
|
|
||||||
|
|
||||||
Auth::logout();
|
Auth::logout();
|
||||||
|
|
||||||
if (! config('ninja.db.multi_db_enabled')) {
|
if (! config('ninja.db.multi_db_enabled')) {
|
||||||
$invoices = Invoice::where('is_deleted', 0)
|
|
||||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
|
||||||
->where('balance', '>', 0)
|
|
||||||
->where('is_proforma', 0)
|
|
||||||
->whereDate('due_date', '<=', now()->addDay()->startOfDay())
|
|
||||||
->whereNull('deleted_at')
|
|
||||||
->whereNotNull('subscription_id')
|
|
||||||
->cursor();
|
|
||||||
|
|
||||||
$invoices->each(function (Invoice $invoice) {
|
nlog('Subscription Cron '. now()->toDateTimeString());
|
||||||
$subscription = $invoice->subscription;
|
|
||||||
|
|
||||||
$body = [
|
$this->timezoneAware();
|
||||||
'context' => 'plan_expired',
|
|
||||||
'client' => $invoice->client->hashed_id,
|
|
||||||
'invoice' => $invoice->hashed_id,
|
|
||||||
'subscription' => $subscription->hashed_id,
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->sendLoad($subscription, $body);
|
|
||||||
//This will send the notification daily.
|
|
||||||
//We'll need to handle this by performing some action on the invoice to either archive it or delete it?
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
//multiDB environment, need to
|
//multiDB environment, need to
|
||||||
foreach (MultiDB::$dbs as $db) {
|
foreach (MultiDB::$dbs as $db) {
|
||||||
MultiDB::setDB($db);
|
MultiDB::setDB($db);
|
||||||
|
|
||||||
$invoices = Invoice::where('is_deleted', 0)
|
nlog('Subscription Cron for ' . $db . ' ' . now()->toDateTimeString());
|
||||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
|
||||||
->where('balance', '>', 0)
|
|
||||||
->where('is_proforma', 0)
|
|
||||||
->whereDate('due_date', '<=', now()->addDay()->startOfDay())
|
|
||||||
->whereNull('deleted_at')
|
|
||||||
->whereNotNull('subscription_id')
|
|
||||||
->cursor();
|
|
||||||
|
|
||||||
$invoices->each(function (Invoice $invoice) {
|
$this->timezoneAware();
|
||||||
$subscription = $invoice->subscription;
|
|
||||||
|
|
||||||
$body = [
|
|
||||||
'context' => 'plan_expired',
|
|
||||||
'client' => $invoice->client->hashed_id,
|
|
||||||
'invoice' => $invoice->hashed_id,
|
|
||||||
'subscription' => $subscription->hashed_id,
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->sendLoad($subscription, $body);
|
|
||||||
//This will send the notification daily.
|
|
||||||
//We'll need to handle this by performing some action on the invoice to either archive it or delete it?
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +94,7 @@ class SubscriptionCron
|
|||||||
->where('is_proforma', 0)
|
->where('is_proforma', 0)
|
||||||
->whereNotNull('subscription_id')
|
->whereNotNull('subscription_id')
|
||||||
->where('balance', '>', 0)
|
->where('balance', '>', 0)
|
||||||
->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay())
|
->whereDate('due_date', '<=', now()->addDay()->startOfDay())
|
||||||
->cursor()
|
->cursor()
|
||||||
->each(function (Invoice $invoice) {
|
->each(function (Invoice $invoice) {
|
||||||
|
|
||||||
|
@ -88,6 +88,7 @@ class CreateRawPdf
|
|||||||
'quote' => $type = 'product',
|
'quote' => $type = 'product',
|
||||||
'credit' => $type = 'product',
|
'credit' => $type = 'product',
|
||||||
'recurring_invoice' => $type = 'product',
|
'recurring_invoice' => $type = 'product',
|
||||||
|
default => $type = 'product',
|
||||||
};
|
};
|
||||||
|
|
||||||
return $type;
|
return $type;
|
||||||
|
@ -70,9 +70,7 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
|
|
||||||
public function backoff()
|
public function backoff()
|
||||||
{
|
{
|
||||||
// return [5, 10, 30, 240];
|
return [rand(5, 29), rand(30, 59), rand(61, 100), rand(180, 500)];
|
||||||
return [rand(5, 10), rand(30, 40), rand(60, 79), rand(160, 400)];
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
@ -182,6 +180,11 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
|
|
||||||
$this->fail();
|
$this->fail();
|
||||||
$this->logMailError($e->getMessage(), $this->company->clients()->first());
|
$this->logMailError($e->getMessage(), $this->company->clients()->first());
|
||||||
|
|
||||||
|
if ($this->nmo->entity) {
|
||||||
|
$this->entityEmailFailed($message);
|
||||||
|
}
|
||||||
|
|
||||||
$this->cleanUpMailers();
|
$this->cleanUpMailers();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -195,6 +198,11 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
|
|
||||||
$this->fail();
|
$this->fail();
|
||||||
$this->logMailError($message, $this->company->clients()->first());
|
$this->logMailError($message, $this->company->clients()->first());
|
||||||
|
|
||||||
|
if ($this->nmo->entity) {
|
||||||
|
$this->entityEmailFailed($message);
|
||||||
|
}
|
||||||
|
|
||||||
$this->cleanUpMailers();
|
$this->cleanUpMailers();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -203,7 +211,7 @@ class NinjaMailerJob implements ShouldQueue
|
|||||||
|
|
||||||
//only report once, not on all tries
|
//only report once, not on all tries
|
||||||
if ($this->attempts() == $this->tries) {
|
if ($this->attempts() == $this->tries) {
|
||||||
/* If the is an entity attached to the message send a failure mailer */
|
/* If there is an entity attached to the message send a failure mailer */
|
||||||
if ($this->nmo->entity) {
|
if ($this->nmo->entity) {
|
||||||
$this->entityEmailFailed($message);
|
$this->entityEmailFailed($message);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Authentication;
|
||||||
|
|
||||||
|
use App\Factory\ClientContactFactory;
|
||||||
|
use App\Factory\ClientFactory;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Utils\Traits\GeneratesCounter;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ClientRegisterService
|
||||||
|
{
|
||||||
|
use GeneratesCounter;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public Company $company,
|
||||||
|
public array $additional = [],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
foreach ($this->company->client_registration_fields as $field) {
|
||||||
|
if ($field['visible'] ?? true) {
|
||||||
|
$rules[$field['key']] = $field['required'] ? ['bail', 'required'] : ['sometimes'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rules as $field => $properties) {
|
||||||
|
if ($field === 'email') {
|
||||||
|
$rules[$field] = array_merge($rules[$field], ['email:rfc,dns', 'max:191', Rule::unique('client_contacts')->where('company_id', $this->company->id)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field === 'current_password' || $field === 'password') {
|
||||||
|
$rules[$field] = array_merge($rules[$field], ['string', 'min:6', 'confirmed']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->company->settings->client_portal_terms || $this->company->settings->client_portal_privacy_policy) {
|
||||||
|
$rules['terms'] = ['required'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->additional as $field) {
|
||||||
|
if ($field['visible'] ?? true) {
|
||||||
|
$rules[$field['key']] = $field['required'] ? ['bail', 'required'] : ['sometimes'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createClient(array $data): Client
|
||||||
|
{
|
||||||
|
$client = ClientFactory::create($this->company->id, $this->company->owner()->id);
|
||||||
|
|
||||||
|
$client->fill($data);
|
||||||
|
|
||||||
|
$client->save();
|
||||||
|
|
||||||
|
if (isset($data['currency_id'])) {
|
||||||
|
$settings = $client->settings;
|
||||||
|
$settings->currency_id = isset($data['currency_id']) ? $data['currency_id'] : $this->company->settings->currency_id;
|
||||||
|
$client->settings = $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client->number = $this->getNextClientNumber($client);
|
||||||
|
$client->save();
|
||||||
|
|
||||||
|
if (!array_key_exists('country_id', $data) && strlen($client->company->settings->country_id) > 1) {
|
||||||
|
$client->update(['country_id' => $client->company->settings->country_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createClientContact(array $data, Client $client): ClientContact
|
||||||
|
{
|
||||||
|
$client_contact = ClientContactFactory::create($this->company->id, $this->company->owner()->id);
|
||||||
|
$client_contact->fill($data);
|
||||||
|
|
||||||
|
$client_contact->client_id = $client->id;
|
||||||
|
$client_contact->is_primary = true;
|
||||||
|
|
||||||
|
if (array_key_exists('password', $data)) {
|
||||||
|
$client_contact->password = Hash::make($data['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$client_contact->save();
|
||||||
|
|
||||||
|
return $client_contact;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function mappings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'contact_first_name' => 'first_name',
|
||||||
|
'contact_last_name' => 'last_name',
|
||||||
|
'contact_email' => 'email',
|
||||||
|
'client_phone' => 'phone',
|
||||||
|
'client_city' => 'city',
|
||||||
|
'client_address_line_1' => 'address1',
|
||||||
|
'client_address_line_2' => 'address2',
|
||||||
|
'client_state' => 'state',
|
||||||
|
'client_country_id' => 'country_id',
|
||||||
|
'client_postal_code' => 'postal_code',
|
||||||
|
'client_shipping_postal_code' => 'shipping_postal_code',
|
||||||
|
'client_shipping_address_line_1' => 'shipping_address1',
|
||||||
|
'client_shipping_city' => 'shipping_city',
|
||||||
|
'client_shipping_state' => 'shipping_state',
|
||||||
|
'client_shipping_country_id' => 'shipping_country_id',
|
||||||
|
'client_custom_value1' => 'custom_value1',
|
||||||
|
'client_custom_value2' => 'custom_value2',
|
||||||
|
'client_custom_value3' => 'custom_value3',
|
||||||
|
'client_custom_value4' => 'custom_value4',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
174
app/Livewire/BillingPortal/Authentication/Login.php
Normal file
174
app/Livewire/BillingPortal/Authentication/Login.php
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Authentication;
|
||||||
|
|
||||||
|
use App\Jobs\Mail\NinjaMailerJob;
|
||||||
|
use App\Jobs\Mail\NinjaMailerObject;
|
||||||
|
use App\Mail\Subscription\OtpCode;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Login extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public ?string $email;
|
||||||
|
|
||||||
|
public ?string $password;
|
||||||
|
|
||||||
|
public ?int $otp;
|
||||||
|
|
||||||
|
public array $state = [
|
||||||
|
'otp' => false, // Use as preference. E-mail/password or OTP.
|
||||||
|
'login_form' => false,
|
||||||
|
'otp_form' => false,
|
||||||
|
'initial_completed' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function initial()
|
||||||
|
{
|
||||||
|
$this->validateOnly('email', ['email' => 'required|bail|email:rfc|email']);
|
||||||
|
|
||||||
|
$contact = ClientContact::where('email', $this->email)
|
||||||
|
->where('company_id', $this->subscription->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($contact === null) {
|
||||||
|
$this->addError('email', ctrans('texts.checkout_only_for_existing_customers'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state['initial_completed'] = true;
|
||||||
|
|
||||||
|
if ($this->state['otp']) {
|
||||||
|
return $this->withOtp();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->withPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPassword()
|
||||||
|
{
|
||||||
|
$contact = ClientContact::where('email', $this->email)
|
||||||
|
->where('company_id', $this->subscription->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($contact) {
|
||||||
|
return $this->state['login_form'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state['login_form'] = false;
|
||||||
|
|
||||||
|
$contact = $this->createClientContact();
|
||||||
|
|
||||||
|
auth()->guard('contact')->loginUsingId($contact->id, true);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: $contact);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withOtp()
|
||||||
|
{
|
||||||
|
$code = rand(100000, 999999);
|
||||||
|
$email_hash = "subscriptions:otp:{$this->email}";
|
||||||
|
|
||||||
|
Cache::put($email_hash, $code, 600);
|
||||||
|
|
||||||
|
$cc = new ClientContact();
|
||||||
|
$cc->email = $this->email;
|
||||||
|
|
||||||
|
$nmo = new NinjaMailerObject();
|
||||||
|
$nmo->mailable = new OtpCode($this->subscription->company, $this->context['contact'] ?? null, $code);
|
||||||
|
$nmo->company = $this->subscription->company;
|
||||||
|
$nmo->settings = $this->subscription->company->settings;
|
||||||
|
$nmo->to_user = $cc;
|
||||||
|
|
||||||
|
NinjaMailerJob::dispatch($nmo);
|
||||||
|
|
||||||
|
if (app()->environment('local')) {
|
||||||
|
session()->flash('message', "[dev]: Your OTP is: {$code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state['otp_form'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleOtp()
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'otp' => 'required|numeric|digits:6 ',
|
||||||
|
'email' => 'required|bail|email:rfc|exists:client_contacts,email',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$code = Cache::get("subscriptions:otp:{$this->email}");
|
||||||
|
|
||||||
|
if ($this->otp != $code) { //loose comparison prevents edge cases
|
||||||
|
$errors = $this->getErrorBag();
|
||||||
|
$errors->add('otp', ctrans('texts.invalid_code'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contact = ClientContact::where('email', $this->email)
|
||||||
|
->where('company_id', $this->subscription->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($contact) {
|
||||||
|
auth()->guard('contact')->loginUsingId($contact->id, true);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: $contact);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handlePassword()
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'email' => 'required|bail|email:rfc',
|
||||||
|
'password' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$attempt = auth()->guard('contact')->attempt([
|
||||||
|
'email' => $this->email,
|
||||||
|
'password' => $this->password,
|
||||||
|
'company_id' => $this->subscription->company_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($attempt) {
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->flash('message', 'These credentials do not match our records.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
if (auth()->guard('contact')->check()) {
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): \Illuminate\View\View
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.authentication.login');
|
||||||
|
}
|
||||||
|
}
|
154
app/Livewire/BillingPortal/Authentication/Register.php
Normal file
154
app/Livewire/BillingPortal/Authentication/Register.php
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Authentication;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Livewire\Component;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
|
||||||
|
class Register extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public ?string $email;
|
||||||
|
|
||||||
|
public ?string $password;
|
||||||
|
|
||||||
|
public ?int $otp;
|
||||||
|
|
||||||
|
public array $state = [
|
||||||
|
'initial_completed' => false,
|
||||||
|
'register_form' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
public array $register_fields = [];
|
||||||
|
|
||||||
|
public array $additional_fields = [];
|
||||||
|
|
||||||
|
public function initial(): void
|
||||||
|
{
|
||||||
|
$this->validateOnly('email', ['email' => 'required|bail|email:rfc']);
|
||||||
|
|
||||||
|
$contact = ClientContact::where('email', $this->email)
|
||||||
|
->where('company_id', $this->subscription->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($contact) {
|
||||||
|
$this->addError('email', ctrans('texts.checkout_only_for_new_customers'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state['initial_completed'] = true;
|
||||||
|
|
||||||
|
$this->registerForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(array $data)
|
||||||
|
{
|
||||||
|
$service = new ClientRegisterService(
|
||||||
|
company: $this->subscription->company,
|
||||||
|
additional: $this->additional_fields,
|
||||||
|
);
|
||||||
|
|
||||||
|
$rules = $service->rules();
|
||||||
|
|
||||||
|
$data = Validator::make($data, $rules)->validate();
|
||||||
|
|
||||||
|
$client = $service->createClient($data);
|
||||||
|
$contact = $service->createClientContact($data, $client);
|
||||||
|
|
||||||
|
auth()->guard('contact')->loginUsingId($contact->id, true);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: $contact);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerForm()
|
||||||
|
{
|
||||||
|
$count = collect($this->subscription->company->client_registration_fields ?? [])
|
||||||
|
->filter(fn($field) => $field['required'] === true || $field['visible'] === true)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
$service = new ClientRegisterService(
|
||||||
|
company: $this->subscription->company,
|
||||||
|
);
|
||||||
|
|
||||||
|
$client = $service->createClient([]);
|
||||||
|
$contact = $service->createClientContact(['email' => $this->email], $client);
|
||||||
|
|
||||||
|
auth()->guard('contact')->loginUsingId($contact->id, true);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: $contact);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->register_fields = [...collect($this->subscription->company->client_registration_fields ?? [])->toArray()];
|
||||||
|
|
||||||
|
$first_gateway = collect($this->subscription->company->company_gateways)
|
||||||
|
->sortBy('sort_order')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$mappings = ClientRegisterService::mappings();
|
||||||
|
|
||||||
|
collect($first_gateway->driver()->getClientRequiredFields() ?? [])
|
||||||
|
->each(function ($field) use ($mappings) {
|
||||||
|
$mapping = $mappings[$field['name']] ?? null;
|
||||||
|
|
||||||
|
if ($mapping === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$i = collect($this->register_fields)->search(fn ($field) => $field['key'] == $mapping);
|
||||||
|
|
||||||
|
if ($i !== false) {
|
||||||
|
$this->register_fields[$i]['visible'] = true;
|
||||||
|
$this->register_fields[$i]['required'] = true;
|
||||||
|
|
||||||
|
|
||||||
|
$this->additional_fields[] = $this->register_fields[$i];
|
||||||
|
} else {
|
||||||
|
$field = [
|
||||||
|
'key' => $mapping,
|
||||||
|
'required' => true,
|
||||||
|
'visible' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->register_fields[] = $field;
|
||||||
|
$this->additional_fields[] = $field;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return $this->state['register_form'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
if (auth()->guard('contact')->check()) {
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.authentication.register');
|
||||||
|
}
|
||||||
|
}
|
261
app/Livewire/BillingPortal/Authentication/RegisterOrLogin.php
Normal file
261
app/Livewire/BillingPortal/Authentication/RegisterOrLogin.php
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Authentication;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Livewire\Component;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Jobs\Mail\NinjaMailerJob;
|
||||||
|
use App\Mail\Subscription\OtpCode;
|
||||||
|
use App\Jobs\Mail\NinjaMailerObject;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class RegisterOrLogin extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public ?string $email;
|
||||||
|
|
||||||
|
public ?string $password;
|
||||||
|
|
||||||
|
public ?int $otp;
|
||||||
|
|
||||||
|
public array $state = [
|
||||||
|
'otp' => false, // Use as preference. E-mail/password or OTP.
|
||||||
|
'login_form' => false,
|
||||||
|
'otp_form' => false,
|
||||||
|
'register_form' => false,
|
||||||
|
'initial_completed' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
public array $register_fields = [];
|
||||||
|
|
||||||
|
public array $additional_fields = [];
|
||||||
|
|
||||||
|
public function initial()
|
||||||
|
{
|
||||||
|
$this->validateOnly('email', ['email' => 'required|bail|email:rfc']);
|
||||||
|
|
||||||
|
$this->state['initial_completed'] = true;
|
||||||
|
|
||||||
|
if ($this->state['otp']) {
|
||||||
|
return $this->withOtp();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->withPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPassword()
|
||||||
|
{
|
||||||
|
$contact = ClientContact::where('email', $this->email)
|
||||||
|
->where('company_id', $this->subscription->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($contact) {
|
||||||
|
return $this->state['login_form'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state['login_form'] = false;
|
||||||
|
$this->registerForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handlePassword()
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'email' => 'required|bail|email:rfc',
|
||||||
|
'password' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$attempt = auth()->guard('contact')->attempt([
|
||||||
|
'email' => $this->email,
|
||||||
|
'password' => $this->password,
|
||||||
|
'company_id' => $this->subscription->company_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($attempt) {
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->flash('message', 'These credentials do not match our records.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withOtp(): void
|
||||||
|
{
|
||||||
|
$contact = ClientContact::where('email', $this->email)
|
||||||
|
->where('company_id', $this->subscription->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($contact === null) {
|
||||||
|
$this->registerForm();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = rand(100000, 999999);
|
||||||
|
$email_hash = "subscriptions:otp:{$this->email}";
|
||||||
|
|
||||||
|
Cache::put($email_hash, $code, 600);
|
||||||
|
|
||||||
|
$cc = new ClientContact();
|
||||||
|
$cc->email = $this->email;
|
||||||
|
|
||||||
|
$nmo = new NinjaMailerObject();
|
||||||
|
$nmo->mailable = new OtpCode($this->subscription->company, $this->context['contact'] ?? null, $code);
|
||||||
|
$nmo->company = $this->subscription->company;
|
||||||
|
$nmo->settings = $this->subscription->company->settings;
|
||||||
|
$nmo->to_user = $cc;
|
||||||
|
|
||||||
|
NinjaMailerJob::dispatch($nmo);
|
||||||
|
|
||||||
|
if (app()->environment('local')) {
|
||||||
|
session()->flash('message', "[dev]: Your OTP is: {$code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state['otp_form'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleOtp(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'otp' => 'required|numeric|digits:6',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$code = Cache::get("subscriptions:otp:{$this->email}");
|
||||||
|
|
||||||
|
if ($this->otp != $code) { //loose comparison prevents edge cases
|
||||||
|
$errors = $this->getErrorBag();
|
||||||
|
$errors->add('otp', ctrans('texts.invalid_code'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contact = ClientContact::where('email', $this->email)
|
||||||
|
->where('company_id', $this->subscription->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($contact) {
|
||||||
|
auth()->guard('contact')->loginUsingId($contact->id, true);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: $contact);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state['otp_form'] = false;
|
||||||
|
$this->registerForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(array $data): void
|
||||||
|
{
|
||||||
|
$service = new ClientRegisterService(
|
||||||
|
company: $this->subscription->company,
|
||||||
|
additional: $this->additional_fields,
|
||||||
|
);
|
||||||
|
|
||||||
|
$rules = $service->rules();
|
||||||
|
$data = Validator::make($data, $rules)->validate();
|
||||||
|
|
||||||
|
$client = $service->createClient($data);
|
||||||
|
$contact = $service->createClientContact($data, $client);
|
||||||
|
|
||||||
|
auth()->guard('contact')->loginUsingId($contact->id, true);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: $contact);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerForm()
|
||||||
|
{
|
||||||
|
$count = collect($this->subscription->company->client_registration_fields ?? [])
|
||||||
|
->filter(fn($field) => $field['required'] === true || $field['visible'] === true)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
$service = new ClientRegisterService(
|
||||||
|
company: $this->subscription->company,
|
||||||
|
);
|
||||||
|
|
||||||
|
$client = $service->createClient([]);
|
||||||
|
$contact = $service->createClientContact(['email' => $this->email], $client);
|
||||||
|
|
||||||
|
auth()->guard('contact')->loginUsingId($contact->id, true);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: $contact);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->register_fields = [...collect($this->subscription->company->client_registration_fields ?? [])->toArray()];
|
||||||
|
|
||||||
|
$first_gateway = collect($this->subscription->company->company_gateways)
|
||||||
|
->sortBy('sort_order')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$mappings = ClientRegisterService::mappings();
|
||||||
|
|
||||||
|
collect($first_gateway->driver()->getClientRequiredFields() ?? [])
|
||||||
|
->each(function ($field) use ($mappings) {
|
||||||
|
$mapping = $mappings[$field['name']] ?? null;
|
||||||
|
|
||||||
|
if ($mapping === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$i = collect($this->register_fields)->search(fn ($field) => $field['key'] == $mapping);
|
||||||
|
|
||||||
|
if ($i !== false) {
|
||||||
|
$this->register_fields[$i]['visible'] = true;
|
||||||
|
$this->register_fields[$i]['required'] = true;
|
||||||
|
|
||||||
|
|
||||||
|
$this->additional_fields[] = $this->register_fields[$i];
|
||||||
|
} else {
|
||||||
|
$field = [
|
||||||
|
'key' => $mapping,
|
||||||
|
'required' => true,
|
||||||
|
'visible' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->register_fields[] = $field;
|
||||||
|
$this->additional_fields[] = $field;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return $this->state['register_form'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
if (auth()->guard('contact')->check()) {
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$countries = Cache::get('countries');
|
||||||
|
|
||||||
|
return view('billing-portal.v3.authentication.register-or-login', [
|
||||||
|
'countries' => $countries,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
44
app/Livewire/BillingPortal/Cart/Cart.php
Normal file
44
app/Livewire/BillingPortal/Cart/Cart.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Cart;
|
||||||
|
|
||||||
|
use App\Libraries\MultiDB;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Cart extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public function handleSubmit()
|
||||||
|
{
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showOptionalProductsLabel()
|
||||||
|
{
|
||||||
|
$optional = [
|
||||||
|
...$this->context['bundle']['optional_recurring_products'] ?? [],
|
||||||
|
...$this->context['bundle']['optional_one_time_products'] ?? [],
|
||||||
|
];
|
||||||
|
|
||||||
|
return count($optional) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.cart.cart');
|
||||||
|
}
|
||||||
|
}
|
33
app/Livewire/BillingPortal/Cart/OneTimeProducts.php
Normal file
33
app/Livewire/BillingPortal/Cart/OneTimeProducts.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Cart;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class OneTimeProducts extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public function quantity($id, $value): void
|
||||||
|
{
|
||||||
|
$this->dispatch('purchase.context', property: "bundle.one_time_products.{$id}.quantity", value: $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.cart.one-time-products');
|
||||||
|
}
|
||||||
|
}
|
33
app/Livewire/BillingPortal/Cart/OptionalOneTimeProducts.php
Normal file
33
app/Livewire/BillingPortal/Cart/OptionalOneTimeProducts.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Cart;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class OptionalOneTimeProducts extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public function quantity($id, $value): void
|
||||||
|
{
|
||||||
|
$this->dispatch('purchase.context', property: "bundle.optional_one_time_products.{$id}.quantity", value: $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): \Illuminate\View\View
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.cart.optional-one-time-products');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Cart;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class OptionalRecurringProducts extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public function quantity($id, $value): void
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: "bundle.optional_recurring_products.{$id}.quantity", value: $value);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): \Illuminate\View\View
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.cart.optional-recurring-products');
|
||||||
|
}
|
||||||
|
}
|
33
app/Livewire/BillingPortal/Cart/RecurringProducts.php
Normal file
33
app/Livewire/BillingPortal/Cart/RecurringProducts.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Cart;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class RecurringProducts extends Component
|
||||||
|
{
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public function quantity($id, $value): void
|
||||||
|
{
|
||||||
|
$this->dispatch('purchase.context', property: "bundle.recurring_products.{$id}.quantity", value: $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): \Illuminate\View\View
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.cart.recurring-products');
|
||||||
|
}
|
||||||
|
}
|
33
app/Livewire/BillingPortal/Example.php
Normal file
33
app/Livewire/BillingPortal/Example.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Example extends Component
|
||||||
|
{
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->dispatch('purchase.context', property: 'quantity', value: 1);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return <<<'HTML'
|
||||||
|
<div>This is step after auth. Currently logged in user is {{ $context['contact']['email'] }}.</div>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
82
app/Livewire/BillingPortal/Payments/Methods.php
Normal file
82
app/Livewire/BillingPortal/Payments/Methods.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal\Payments;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class Methods extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public array $methods;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$total = collect($this->context['products'])->sum('total_raw');
|
||||||
|
|
||||||
|
$methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods(
|
||||||
|
$total,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->methods = $methods;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleSelect(string $company_gateway_id, string $gateway_type_id)
|
||||||
|
{
|
||||||
|
/** @var \App\Models\ClientContact $contact */
|
||||||
|
$contact = auth()->guard('contact')->user();
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'client_id', value: $contact->client->hashed_id);
|
||||||
|
|
||||||
|
$this->context['client_id'] = $contact->client->hashed_id;
|
||||||
|
|
||||||
|
$invoice = $this->subscription
|
||||||
|
->calc()
|
||||||
|
->buildPurchaseInvoice($this->context)
|
||||||
|
->service()
|
||||||
|
->markSent()
|
||||||
|
->fillDefaults()
|
||||||
|
->adjustInventory()
|
||||||
|
->save();
|
||||||
|
|
||||||
|
Cache::put($this->context['hash'], [
|
||||||
|
'subscription_id' => $this->subscription->hashed_id,
|
||||||
|
'email' => $contact->email,
|
||||||
|
'client_id' => $contact->client->hashed_id,
|
||||||
|
'invoice_id' => $invoice->hashed_id,
|
||||||
|
'context' => 'purchase',
|
||||||
|
'campaign' => $this->context['campaign'],
|
||||||
|
'bundle' => $this->context['bundle'],
|
||||||
|
], now()->addMinutes(60));
|
||||||
|
|
||||||
|
$payable_amount = $invoice->partial > 0
|
||||||
|
? \App\Utils\Number::formatValue($invoice->partial, $invoice->client->currency())
|
||||||
|
: \App\Utils\Number::formatValue($invoice->balance, $invoice->client->currency());
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'form.company_gateway_id', value: $company_gateway_id);
|
||||||
|
$this->dispatch('purchase.context', property: 'form.payment_method_id', value: $gateway_type_id);
|
||||||
|
$this->dispatch('purchase.context', property: 'form.invoice_hashed_id', value: $invoice->hashed_id);
|
||||||
|
$this->dispatch('purchase.context', property: 'form.payable_amount', value: $payable_amount);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.payments.methods');
|
||||||
|
}
|
||||||
|
}
|
169
app/Livewire/BillingPortal/Purchase.php
Normal file
169
app/Livewire/BillingPortal/Purchase.php
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal;
|
||||||
|
|
||||||
|
use App\Libraries\MultiDB;
|
||||||
|
use App\Livewire\BillingPortal\Authentication\Login;
|
||||||
|
use App\Livewire\BillingPortal\Authentication\Register;
|
||||||
|
use App\Livewire\BillingPortal\Authentication\RegisterOrLogin;
|
||||||
|
use App\Livewire\BillingPortal\Cart\Cart;
|
||||||
|
use App\Livewire\BillingPortal\Payments\Methods;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Livewire\Attributes\Computed;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class Purchase extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public string $db;
|
||||||
|
|
||||||
|
public array $request_data;
|
||||||
|
|
||||||
|
public string $hash;
|
||||||
|
|
||||||
|
public ?string $campaign;
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
public int $step = 0;
|
||||||
|
|
||||||
|
public string $id;
|
||||||
|
|
||||||
|
public static array $dependencies = [
|
||||||
|
Login::class => [
|
||||||
|
'id' => 'auth.login',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
RegisterOrLogin::class => [
|
||||||
|
'id' => 'auth.login-or-register',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
Register::class => [
|
||||||
|
'id' => 'auth.register',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
Cart::class => [
|
||||||
|
'id' => 'cart',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public array $steps = [];
|
||||||
|
|
||||||
|
public array $context = [];
|
||||||
|
|
||||||
|
#[On('purchase.context')]
|
||||||
|
public function handleContext(string $property, $value): self
|
||||||
|
{
|
||||||
|
$clone = $this->context;
|
||||||
|
|
||||||
|
data_set($this->context, $property, $value);
|
||||||
|
|
||||||
|
// The following may not be needed, as we can pass arround $context.
|
||||||
|
// cache()->set($this->hash, $this->context);
|
||||||
|
|
||||||
|
if ($clone !== $this->context) {
|
||||||
|
$this->id = Str::uuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('purchase.next')]
|
||||||
|
public function handleNext(): void
|
||||||
|
{
|
||||||
|
if (count($this->steps) >= 1 && $this->step < count($this->steps) - 1) {
|
||||||
|
$this->step++;
|
||||||
|
$this->id = Str::uuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('purchase.forward')]
|
||||||
|
public function handleForward(string $component): void
|
||||||
|
{
|
||||||
|
$this->step = array_search($component, $this->steps);
|
||||||
|
|
||||||
|
$this->id = Str::uuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed()]
|
||||||
|
public function component(): string
|
||||||
|
{
|
||||||
|
return $this->steps[$this->step];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed()]
|
||||||
|
public function componentUniqueId(): string
|
||||||
|
{
|
||||||
|
return "purchase-{$this->id}";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed()]
|
||||||
|
public function summaryUniqueId(): string
|
||||||
|
{
|
||||||
|
return "summary-{$this->id}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function defaultSteps()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Cart::class,
|
||||||
|
RegisterOrLogin::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$classes = collect(self::$dependencies)->mapWithKeys(fn($dependency, $class) => [$dependency['id'] => $class])->toArray();
|
||||||
|
|
||||||
|
if ($this->subscription->steps) {
|
||||||
|
$steps = collect(explode(',', $this->subscription->steps))
|
||||||
|
->map(fn($step) => $classes[$step])
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$this->steps = [
|
||||||
|
Setup::class,
|
||||||
|
...$steps,
|
||||||
|
Methods::class,
|
||||||
|
RFF::class,
|
||||||
|
Submit::class,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$this->steps = [
|
||||||
|
Setup::class,
|
||||||
|
...self::defaultSteps(),
|
||||||
|
Methods::class,
|
||||||
|
RFF::class,
|
||||||
|
Submit::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->id = Str::uuid();
|
||||||
|
|
||||||
|
MultiDB::setDb($this->db);
|
||||||
|
|
||||||
|
$this
|
||||||
|
->handleContext('hash', $this->hash)
|
||||||
|
->handleContext('quantity', 1)
|
||||||
|
->handleContext('request_data', $this->request_data)
|
||||||
|
->handleContext('campaign', $this->campaign);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.purchase');
|
||||||
|
}
|
||||||
|
}
|
83
app/Livewire/BillingPortal/RFF.php
Normal file
83
app/Livewire/BillingPortal/RFF.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal;
|
||||||
|
|
||||||
|
use App\Models\CompanyGateway;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class RFF extends Component
|
||||||
|
{
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public string $contact_first_name;
|
||||||
|
|
||||||
|
public string $contact_last_name;
|
||||||
|
|
||||||
|
public string $contact_email;
|
||||||
|
|
||||||
|
#[On('passed-required-fields-check')]
|
||||||
|
public function continue(): void
|
||||||
|
{
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
|
||||||
|
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleSubmit()
|
||||||
|
{
|
||||||
|
$data = $this->validate([
|
||||||
|
'contact_first_name' => 'required',
|
||||||
|
'contact_last_name' => 'required',
|
||||||
|
'contact_email' => 'required|email:rfc',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contact = auth()->guard('contact');
|
||||||
|
|
||||||
|
$contact->user()->update([
|
||||||
|
'first_name' => $data['contact_first_name'],
|
||||||
|
'last_name' => $data['contact_last_name'],
|
||||||
|
'email' => $data['contact_email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
|
||||||
|
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->contact_first_name = $this->context['contact']['first_name'] ?? '';
|
||||||
|
$this->contact_last_name = $this->context['contact']['last_name'] ?? '';
|
||||||
|
$this->contact_email = $this->context['contact']['email'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$gateway = CompanyGateway::find($this->context['form']['company_gateway_id']);
|
||||||
|
$countries = Cache::get('countries');
|
||||||
|
|
||||||
|
if ($gateway === null) {
|
||||||
|
return view('billing-portal.v3.rff-basic');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('billing-portal.v3.rff', [
|
||||||
|
'gateway' => $gateway->driver(
|
||||||
|
auth()->guard('contact')->user()->client
|
||||||
|
),
|
||||||
|
'countries' => $countries,
|
||||||
|
'company' => $gateway->company,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
33
app/Livewire/BillingPortal/Setup.php
Normal file
33
app/Livewire/BillingPortal/Setup.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Setup extends Component
|
||||||
|
{
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->dispatch('purchase.context', property: 'quantity', value: 1);
|
||||||
|
$this->dispatch('purchase.next');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return <<<'HTML'
|
||||||
|
<template></template>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
76
app/Livewire/BillingPortal/Submit.php
Normal file
76
app/Livewire/BillingPortal/Submit.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\Attributes\Lazy;
|
||||||
|
use App\Services\ClientPortal\InstantPayment;
|
||||||
|
|
||||||
|
class Submit extends Component
|
||||||
|
{
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
|
||||||
|
// $request = new \Illuminate\Http\Request([
|
||||||
|
// 'sidebar' => 'hidden',
|
||||||
|
// 'hash' => $this->context['hash'],
|
||||||
|
// 'action' => 'payment',
|
||||||
|
// 'invoices' => [
|
||||||
|
// $this->context['form']['invoice_hashed_id'],
|
||||||
|
// ],
|
||||||
|
// 'payable_invoices' => [
|
||||||
|
// [
|
||||||
|
// 'amount' => $this->context['form']['payable_amount'],
|
||||||
|
// 'invoice_id' => $this->context['form']['invoice_hashed_id'],
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// 'company_gateway_id' => $this->context['form']['company_gateway_id'],
|
||||||
|
// 'payment_method_id' => $this->context['form']['payment_method_id'],
|
||||||
|
// 'contact_first_name' => $this->context['contact']['first_name'],
|
||||||
|
// 'contact_last_name' => $this->context['contact']['last_name'],
|
||||||
|
// 'contact_email' => $this->context['contact']['email'],
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// return redirect((new InstantPayment($request))->run());
|
||||||
|
// dd($this->context);
|
||||||
|
|
||||||
|
nlog($this->context);
|
||||||
|
|
||||||
|
$this->dispatch(
|
||||||
|
'purchase.submit',
|
||||||
|
invoice_hashed_id: $this->context['form']['invoice_hashed_id'],
|
||||||
|
payable_amount: $this->context['form']['payable_amount'],
|
||||||
|
company_gateway_id: $this->context['form']['company_gateway_id'],
|
||||||
|
payment_method_id: $this->context['form']['payment_method_id'],
|
||||||
|
contact_first_name: $this->context['contact']['first_name'],
|
||||||
|
contact_last_name: $this->context['contact']['last_name'],
|
||||||
|
contact_email: $this->context['contact']['email'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
|
||||||
|
return <<<'HTML'
|
||||||
|
<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>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
188
app/Livewire/BillingPortal/Summary.php
Normal file
188
app/Livewire/BillingPortal/Summary.php
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Livewire\BillingPortal;
|
||||||
|
|
||||||
|
use App\Models\RecurringInvoice;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use App\Utils\Number;
|
||||||
|
use Livewire\Attributes\Computed;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Summary extends Component
|
||||||
|
{
|
||||||
|
public Subscription $subscription;
|
||||||
|
|
||||||
|
public array $context;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$bundle = $this->context['bundle'] ?? [
|
||||||
|
'recurring_products' => [],
|
||||||
|
'optional_recurring_products' => [],
|
||||||
|
'one_time_products' => [],
|
||||||
|
'optional_one_time_products' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($this->subscription->service()->recurring_products() as $key => $product) {
|
||||||
|
$bundle['recurring_products'][$product->hashed_id] = [
|
||||||
|
'product' => $product,
|
||||||
|
'quantity' => $bundle['recurring_products'][$product->hashed_id]['quantity'] ?? 1,
|
||||||
|
'notes' => $product->markdownNotes(),
|
||||||
|
];
|
||||||
|
$bundle['recurring_products'][$product->hashed_id]['product']['is_recurring'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->subscription->service()->products() as $key => $product) {
|
||||||
|
$bundle['one_time_products'][$product->hashed_id] = [
|
||||||
|
'product' => $product,
|
||||||
|
'quantity' => $bundle['one_time_products'][$product->hashed_id]['quantity'] ?? 1,
|
||||||
|
'notes' => $product->markdownNotes(),
|
||||||
|
];
|
||||||
|
$bundle['one_time_products'][$product->hashed_id]['product']['is_recurring'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->subscription->service()->optional_recurring_products() as $key => $product) {
|
||||||
|
$bundle['optional_recurring_products'][$product->hashed_id] = [
|
||||||
|
'product' => $product,
|
||||||
|
'quantity' => $bundle['optional_recurring_products'][$product->hashed_id]['quantity'] ?? 0,
|
||||||
|
'notes' => $product->markdownNotes(),
|
||||||
|
];
|
||||||
|
$bundle['optional_recurring_products'][$product->hashed_id]['product']['is_recurring'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->subscription->service()->optional_products() as $key => $product) {
|
||||||
|
$bundle['optional_one_time_products'][$product->hashed_id] = [
|
||||||
|
'product' => $product,
|
||||||
|
'quantity' => $bundle['optional_one_time_products'][$product->hashed_id]['quantity'] ?? 0,
|
||||||
|
'notes' => $product->markdownNotes(),
|
||||||
|
];
|
||||||
|
$bundle['optional_one_time_products'][$product->hashed_id]['product']['is_recurring'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'bundle', value: $bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function oneTimePurchasesTotal(bool $raw = false)
|
||||||
|
{
|
||||||
|
if (isset($this->context['bundle']['recurring_products']) === false) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$one_time = collect($this->context['bundle']['one_time_products'])->sum(function ($item) {
|
||||||
|
return $item['product']['price'] * $item['quantity'];
|
||||||
|
});
|
||||||
|
|
||||||
|
$one_time_optional = collect($this->context['bundle']['optional_one_time_products'])->sum(function ($item) {
|
||||||
|
return $item['product']['price'] * $item['quantity'];
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($raw) {
|
||||||
|
return $one_time + $one_time_optional;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number::formatMoney($one_time + $one_time_optional, $this->subscription->company);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recurringPurchasesTotal(bool $raw = false)
|
||||||
|
{
|
||||||
|
if (isset($this->context['bundle']['recurring_products']) === false) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$recurring = collect($this->context['bundle']['recurring_products'])->sum(function ($item) {
|
||||||
|
return $item['product']['price'] * $item['quantity'];
|
||||||
|
});
|
||||||
|
|
||||||
|
$recurring_optional = collect($this->context['bundle']['optional_recurring_products'])->sum(function ($item) {
|
||||||
|
return $item['product']['price'] * $item['quantity'];
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($raw) {
|
||||||
|
return $recurring + $recurring_optional;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \sprintf(
|
||||||
|
'%s/%s',
|
||||||
|
Number::formatMoney($recurring + $recurring_optional, $this->subscription->company),
|
||||||
|
RecurringInvoice::frequencyForKey($this->subscription->frequency_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed()]
|
||||||
|
public function total()
|
||||||
|
{
|
||||||
|
return Number::formatMoney(
|
||||||
|
collect([
|
||||||
|
$this->oneTimePurchasesTotal(raw: true),
|
||||||
|
$this->recurringPurchasesTotal(raw: true),
|
||||||
|
])->sum(),
|
||||||
|
$this->subscription->company
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
if (isset($this->context['bundle']) === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = [];
|
||||||
|
|
||||||
|
foreach ($this->context['bundle']['recurring_products'] as $key => $item) {
|
||||||
|
$products[] = [
|
||||||
|
'product_key' => $item['product']['product_key'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'total_raw' => $item['product']['price'] * $item['quantity'],
|
||||||
|
'total' => Number::formatMoney($item['product']['price'] * $item['quantity'], $this->subscription->company) . ' / ' . RecurringInvoice::frequencyForKey($this->subscription->frequency_id),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->context['bundle']['optional_recurring_products'] as $key => $item) {
|
||||||
|
$products[] = [
|
||||||
|
'product_key' => $item['product']['product_key'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'total_raw' => $item['product']['price'] * $item['quantity'],
|
||||||
|
'total' => Number::formatMoney($item['product']['price'] * $item['quantity'], $this->subscription->company) . ' / ' . RecurringInvoice::frequencyForKey($this->subscription->frequency_id),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->context['bundle']['one_time_products'] as $key => $item) {
|
||||||
|
$products[] = [
|
||||||
|
'product_key' => $item['product']['product_key'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'total_raw' => $item['product']['price'] * $item['quantity'],
|
||||||
|
'total' => Number::formatMoney($item['product']['price'] * $item['quantity'], $this->subscription->company),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->context['bundle']['optional_one_time_products'] as $key => $item) {
|
||||||
|
$products[] = [
|
||||||
|
'product_key' => $item['product']['product_key'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'total_raw' => $item['product']['price'] * $item['quantity'],
|
||||||
|
'total' => Number::formatMoney($item['product']['price'] * $item['quantity'], $this->subscription->company),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatch('purchase.context', property: 'products', value: $products);
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('billing-portal.v3.summary');
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Client;
|
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use App\Libraries\MultiDB;
|
use App\Libraries\MultiDB;
|
||||||
@ -186,6 +185,8 @@ class RequiredClientInfo extends Component
|
|||||||
|
|
||||||
public $company_gateway_id;
|
public $company_gateway_id;
|
||||||
|
|
||||||
|
public bool $form_only = false;
|
||||||
|
|
||||||
public $db;
|
public $db;
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
@ -222,7 +223,7 @@ class RequiredClientInfo extends Component
|
|||||||
$this->show_form = true;
|
$this->show_form = true;
|
||||||
|
|
||||||
$hash = Cache::get(request()->input('hash'));
|
$hash = Cache::get(request()->input('hash'));
|
||||||
|
|
||||||
/** @var \App\Models\Invoice $invoice */
|
/** @var \App\Models\Invoice $invoice */
|
||||||
$invoice = Invoice::find($this->decodePrimaryKey($hash['invoice_id']));
|
$invoice = Invoice::find($this->decodePrimaryKey($hash['invoice_id']));
|
||||||
|
|
||||||
@ -232,6 +233,15 @@ class RequiredClientInfo extends Component
|
|||||||
count($this->fields) > 0 || $this->show_terms
|
count($this->fields) > 0 || $this->show_terms
|
||||||
? $this->checkFields()
|
? $this->checkFields()
|
||||||
: $this->show_form = false;
|
: $this->show_form = false;
|
||||||
|
|
||||||
|
if (request()->query('source') === 'subscriptions') {
|
||||||
|
$this->show_form = false;
|
||||||
|
|
||||||
|
$this->dispatch(
|
||||||
|
'passed-required-fields-check',
|
||||||
|
client_postal_code: $this->contact->client->postal_code
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Computed]
|
#[Computed]
|
||||||
@ -240,7 +250,7 @@ class RequiredClientInfo extends Component
|
|||||||
|
|
||||||
MultiDB::setDb($this->db);
|
MultiDB::setDb($this->db);
|
||||||
return ClientContact::withTrashed()->find($this->contact_id);
|
return ClientContact::withTrashed()->find($this->contact_id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Computed]
|
#[Computed]
|
||||||
@ -259,7 +269,7 @@ class RequiredClientInfo extends Component
|
|||||||
|
|
||||||
public function handleSubmit(array $data): bool
|
public function handleSubmit(array $data): bool
|
||||||
{
|
{
|
||||||
|
|
||||||
MultiDB::setDb($this->db);
|
MultiDB::setDb($this->db);
|
||||||
$contact = ClientContact::withTrashed()->find($this->contact_id);
|
$contact = ClientContact::withTrashed()->find($this->contact_id);
|
||||||
|
|
||||||
@ -367,7 +377,6 @@ $_contact->push();
|
|||||||
|
|
||||||
public function checkFields()
|
public function checkFields()
|
||||||
{
|
{
|
||||||
|
|
||||||
MultiDB::setDb($this->db);
|
MultiDB::setDb($this->db);
|
||||||
$_contact = ClientContact::withTrashed()->find($this->contact_id);
|
$_contact = ClientContact::withTrashed()->find($this->contact_id);
|
||||||
|
|
||||||
@ -375,7 +384,10 @@ $_contact->push();
|
|||||||
$_field = $this->mappings[$field['name']];
|
$_field = $this->mappings[$field['name']];
|
||||||
|
|
||||||
if (Str::startsWith($field['name'], 'client_')) {
|
if (Str::startsWith($field['name'], 'client_')) {
|
||||||
if (empty($_contact->client->{$_field}) || is_null($_contact->client->{$_field}) || in_array($_field, $this->client_address_array)) {
|
if (empty($_contact->client->{$_field})
|
||||||
|
|| is_null($_contact->client->{$_field})
|
||||||
|
// || in_array($_field, $this->client_address_array)
|
||||||
|
) {
|
||||||
$this->show_form = true;
|
$this->show_form = true;
|
||||||
} else {
|
} else {
|
||||||
$this->fields[$index]['filled'] = true;
|
$this->fields[$index]['filled'] = true;
|
||||||
@ -390,6 +402,17 @@ $_contact->push();
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$left = collect($this->fields)
|
||||||
|
->filter(fn ($field) => !array_key_exists('filled', $field))
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($left === 0) {
|
||||||
|
$this->dispatch(
|
||||||
|
'passed-required-fields-check',
|
||||||
|
client_postal_code: $this->contact->client->postal_code
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function showCopyBillingCheckbox(): bool
|
public function showCopyBillingCheckbox(): bool
|
||||||
|
@ -58,6 +58,8 @@ class OtpCode extends Mailable
|
|||||||
'title' => ctrans('texts.otp_code_subject'),
|
'title' => ctrans('texts.otp_code_subject'),
|
||||||
'content' => ctrans('texts.otp_code_body', ['code' => $this->code]),
|
'content' => ctrans('texts.otp_code_body', ['code' => $this->code]),
|
||||||
'whitelabel' => $this->company->account->isPaid(),
|
'whitelabel' => $this->company->account->isPaid(),
|
||||||
|
'url' => 'xx',
|
||||||
|
'button' => false,
|
||||||
'template' => $this->company->account->isPremium() ? 'email.template.admin_premium' : 'email.template.admin',
|
'template' => $this->company->account->isPremium() ? 'email.template.admin_premium' : 'email.template.admin',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -291,7 +291,7 @@ class Account extends BaseModel
|
|||||||
|
|
||||||
public function isPaid(): bool
|
public function isPaid(): bool
|
||||||
{
|
{
|
||||||
return Ninja::isNinja() ? ($this->isPaidHostedClient() && !$this->isTrial()) : $this->hasFeature(self::FEATURE_WHITE_LABEL);
|
return Ninja::isNinja() ? $this->isPaidHostedClient() : $this->hasFeature(self::FEATURE_WHITE_LABEL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPremium(): bool
|
public function isPremium(): bool
|
||||||
|
@ -190,7 +190,7 @@ class Expense extends BaseModel
|
|||||||
|
|
||||||
public function purchase_order()
|
public function purchase_order()
|
||||||
{
|
{
|
||||||
return $this->hasOne(PurchaseOrder::class);
|
return $this->hasOne(PurchaseOrder::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function translate_entity()
|
public function translate_entity()
|
||||||
|
@ -99,6 +99,64 @@ class Product extends BaseModel
|
|||||||
'tax_id',
|
'tax_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public array $ubl_tax_map = [
|
||||||
|
self::PRODUCT_TYPE_REVERSE_TAX => 'AE', // VAT_REVERSE_CHARGE =
|
||||||
|
self::PRODUCT_TYPE_EXEMPT => 'E', // EXEMPT_FROM_TAX =
|
||||||
|
self::PRODUCT_TYPE_PHYSICAL => 'S', // STANDARD_RATE =
|
||||||
|
self::PRODUCT_TYPE_ZERO_RATED => 'Z', // ZERO_RATED_GOODS =
|
||||||
|
// self::PRODUCT_TYPE_ZERO_RATED => 'G', // FREE_EXPORT_ITEM =
|
||||||
|
// self::PRODUCT_TYPE_ZERO_RATED => 'O', // OUTSIDE_TAX_SCOPE =
|
||||||
|
// self::PRODUCT_TYPE_EXEMPT => 'K', // EEA_GOODS_AND_SERVICES =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'L', // CANARY_ISLANDS_INDIRECT_TAX =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'M', // CEUTA_AND_MELILLA =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'B', // TRANSFERRED_VAT_ITALY =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'A', // MIXED_TAX_RATE =
|
||||||
|
self::PRODUCT_TYPE_REDUCED_TAX => 'AA', // LOWER_RATE =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'AB', // EXEMPT_FOR_RESALE =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'AC', // VAT_NOT_NOW_DUE =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'AD', // VAT_DUE_PREVIOUS_INVOICE =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'B', // TRANSFERRED_VAT =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'C', // DUTY_PAID_BY_SUPPLIER =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'D', // VAT_MARGIN_SCHEME_TRAVEL_AGENTS =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'F', // VAT_MARGIN_SCHEME_SECOND_HAND_GOODS =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'H', // HIGHER_RATE =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'I', // VAT_MARGIN_SCHEME_WORKS_OF_ART =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'J', // VAT_MARGIN_SCHEME_COLLECTORS_ITEMS =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'K', // VAT_EXEMPT_EEA_INTRA_COMMUNITY =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'L', // CANARY_ISLANDS_TAX =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'M', // TAX_CEUTA_MELILLA =
|
||||||
|
// self::PRODUCT_TYPE_PHYSICAL => 'O', // SERVICES_OUTSIDE_SCOPE =
|
||||||
|
];
|
||||||
|
|
||||||
|
public array $ubl_tax_translations = [
|
||||||
|
'texts.reverse_tax' => 'AE', // VAT_REVERSE_CHARGE
|
||||||
|
'texts.tax_exempt' => 'E', // EXEMPT_FROM_TAX
|
||||||
|
'texts.physical_goods' => 'S', // STANDARD_RATE
|
||||||
|
'texts.zero_rated' => 'Z', // ZERO_RATED_GOODS
|
||||||
|
'ubl.vat_exempt_eea_intra_community' => 'K', // VAT_EXEMPT_EEA_INTRA_COMMUNITY
|
||||||
|
'ubl.free_export_item' => 'G', // FREE_EXPORT_ITEM
|
||||||
|
'ubl.outside_tax_scope' => 'O', // OUTSIDE_TAX_SCOPE
|
||||||
|
'ubl.eea_goods_and_services' => 'K', // EEA_GOODS_AND_SERVICES
|
||||||
|
'ubl.canary_islands_indirect_tax' => 'L', // CANARY_ISLANDS_INDIRECT_TAX
|
||||||
|
'ubl.ceuta_and_melilla' => 'M', // CEUTA_AND_MELILLA
|
||||||
|
'ubl.transferred_vat_italy' => 'B', // TRANSFERRED_VAT_ITALY
|
||||||
|
'ubl.mixed_tax_rate' => 'A', // MIXED_TAX_RATE
|
||||||
|
'ubl.lower_rate' => 'AA', // LOWER_RATE
|
||||||
|
'ubl.exempt_for_resale' => 'AB', // EXEMPT_FOR_RESALE
|
||||||
|
'ubl.vat_not_now_due' => 'AC', // VAT_NOT_NOW_DUE
|
||||||
|
'ubl.vat_due_previous_invoice' => 'AD', // VAT_DUE_PREVIOUS_INVOICE
|
||||||
|
'ubl.transferred_vat' => 'B', // TRANSFERRED_VAT
|
||||||
|
'ubl.duty_paid_by_supplier' => 'C', // DUTY_PAID_BY_SUPPLIER
|
||||||
|
'ubl.vat_margin_scheme_travel_agents' => 'D', // VAT_MARGIN_SCHEME_TRAVEL_AGENTS
|
||||||
|
'ubl.vat_margin_scheme_second_hand_goods' => 'F', // VAT_MARGIN_SCHEME_SECOND_HAND_GOODS
|
||||||
|
'ubl.higher_rate' => 'H', // HIGHER_RATE
|
||||||
|
'ubl.vat_margin_scheme_works_of_art' => 'I', // VAT_MARGIN_SCHEME_WORKS_OF_ART
|
||||||
|
'ubl.vat_margin_scheme_collectors_items' => 'J', // VAT_MARGIN_SCHEME_COLLECTORS_ITEMS
|
||||||
|
'ubl.canary_islands_tax' => 'L', // CANARY_ISLANDS_TAX
|
||||||
|
'ubl.tax_ceuta_melilla' => 'M', // TAX_CEUTA_MELILLA
|
||||||
|
'ubl.services_outside_scope' => 'O', // SERVICES_OUTSIDE_SCOPE
|
||||||
|
];
|
||||||
|
|
||||||
protected $touches = [];
|
protected $touches = [];
|
||||||
|
|
||||||
public function getEntityType()
|
public function getEntityType()
|
||||||
@ -148,6 +206,20 @@ class Product extends BaseModel
|
|||||||
return $converter->convert($this->notes ?? '');
|
return $converter->convert($this->notes ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function markdownHelp(string $notes = '')
|
||||||
|
{
|
||||||
|
|
||||||
|
$converter = new CommonMarkConverter([
|
||||||
|
'allow_unsafe_links' => false,
|
||||||
|
'renderer' => [
|
||||||
|
'soft_break' => '<br>',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $converter->convert($notes);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function portalUrl($use_react_url): string
|
public function portalUrl($use_react_url): string
|
||||||
{
|
{
|
||||||
return $use_react_url ? config('ninja.react_url') . "/#/products/{$this->hashed_id}/edit" : config('ninja.app_url');
|
return $use_react_url ? config('ninja.react_url') . "/#/products/{$this->hashed_id}/edit" : config('ninja.app_url');
|
||||||
|
@ -265,7 +265,7 @@ class PurchaseOrder extends BaseModel
|
|||||||
|
|
||||||
public function expense(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
public function expense(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Expense::class);
|
return $this->belongsTo(Expense::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Services\Subscription\PaymentLinkService;
|
use App\Services\Subscription\PaymentLinkService;
|
||||||
|
use App\Services\Subscription\SubscriptionCalculator;
|
||||||
use App\Services\Subscription\SubscriptionService;
|
use App\Services\Subscription\SubscriptionService;
|
||||||
use App\Services\Subscription\SubscriptionStatus;
|
use App\Services\Subscription\SubscriptionStatus;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@ -115,6 +116,7 @@ class Subscription extends BaseModel
|
|||||||
'optional_product_ids',
|
'optional_product_ids',
|
||||||
'optional_recurring_product_ids',
|
'optional_recurring_product_ids',
|
||||||
'use_inventory_management',
|
'use_inventory_management',
|
||||||
|
'steps',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@ -146,6 +148,11 @@ class Subscription extends BaseModel
|
|||||||
return (new SubscriptionStatus($this, $recurring_invoice))->run();
|
return (new SubscriptionStatus($this, $recurring_invoice))->run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function calc(): SubscriptionCalculator
|
||||||
|
{
|
||||||
|
return new SubscriptionCalculator($this);
|
||||||
|
}
|
||||||
|
|
||||||
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Company::class);
|
return $this->belongsTo(Company::class);
|
||||||
|
@ -103,7 +103,7 @@ class AuthorizeCustomer
|
|||||||
} else {
|
} else {
|
||||||
// nlog("creating client");
|
// nlog("creating client");
|
||||||
|
|
||||||
$first_payment_profile = $profile['payment_profiles'][0];
|
$first_payment_profile = &$profile['payment_profiles'][0];
|
||||||
|
|
||||||
if (! $first_payment_profile) {
|
if (! $first_payment_profile) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -101,9 +101,10 @@ class AuthorizePaymentMethod
|
|||||||
$gateway_customer_reference = (new AuthorizeCreateCustomer($this->authorize, $this->authorize->client))->create($data);
|
$gateway_customer_reference = (new AuthorizeCreateCustomer($this->authorize, $this->authorize->client))->create($data);
|
||||||
$payment_profile = $this->addPaymentMethodToClient($gateway_customer_reference, $data);
|
$payment_profile = $this->addPaymentMethodToClient($gateway_customer_reference, $data);
|
||||||
|
|
||||||
$this->createClientGatewayToken($payment_profile, $gateway_customer_reference);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->createClientGatewayToken($payment_profile, $gateway_customer_reference);
|
||||||
|
|
||||||
return redirect()->route('client.payment_methods.index');
|
return redirect()->route('client.payment_methods.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +169,9 @@ class AuthorizePaymentMethod
|
|||||||
$billto->setCity(substr($this->authorize->client->city, 0, 40));
|
$billto->setCity(substr($this->authorize->client->city, 0, 40));
|
||||||
$billto->setState(substr($this->authorize->client->state, 0, 40));
|
$billto->setState(substr($this->authorize->client->state, 0, 40));
|
||||||
$billto->setZip(substr($this->authorize->client->postal_code, 0, 20));
|
$billto->setZip(substr($this->authorize->client->postal_code, 0, 20));
|
||||||
|
|
||||||
|
if(isset($contact->email) && str_contains($contact->email, '@'))
|
||||||
|
$billto->setEmail($contact->email);
|
||||||
|
|
||||||
if ($this->authorize->client->country_id) {
|
if ($this->authorize->client->country_id) {
|
||||||
$billto->setCountry($this->authorize->client->country->name);
|
$billto->setCountry($this->authorize->client->country->name);
|
||||||
@ -179,7 +183,7 @@ class AuthorizePaymentMethod
|
|||||||
// Create a new Customer Payment Profile object
|
// Create a new Customer Payment Profile object
|
||||||
$paymentprofile = new CustomerPaymentProfileType();
|
$paymentprofile = new CustomerPaymentProfileType();
|
||||||
$paymentprofile->setCustomerType('individual');
|
$paymentprofile->setCustomerType('individual');
|
||||||
|
|
||||||
if ($billto) {
|
if ($billto) {
|
||||||
$paymentprofile->setBillTo($billto);
|
$paymentprofile->setBillTo($billto);
|
||||||
}
|
}
|
||||||
|
@ -195,6 +195,8 @@ class AuthorizePaymentDriver extends BaseDriver
|
|||||||
{
|
{
|
||||||
$this->init();
|
$this->init();
|
||||||
|
|
||||||
|
nlog("starting import auth.net");
|
||||||
|
|
||||||
return (new AuthorizeCustomer($this))->importCustomers();
|
return (new AuthorizeCustomer($this))->importCustomers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,7 +387,6 @@ class StripePaymentDriver extends BaseDriver
|
|||||||
$fields[] = ['name' => 'client_custom_value4', 'label' => $this->helpers->makeCustomField($this->client->company->custom_fields, 'client4'), 'type' => 'text', 'validation' => 'required'];
|
$fields[] = ['name' => 'client_custom_value4', 'label' => $this->helpers->makeCustomField($this->client->company->custom_fields, 'client4'), 'type' => 'text', 'validation' => 'required'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class ActivityRepository extends BaseRepository
|
|||||||
* Save the Activity.
|
* Save the Activity.
|
||||||
*
|
*
|
||||||
* @param \stdClass $fields The fields
|
* @param \stdClass $fields The fields
|
||||||
* @param \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder $entity
|
* @param \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\Expense $entity
|
||||||
* @param array $event_vars
|
* @param array $event_vars
|
||||||
*/
|
*/
|
||||||
public function save($fields, $entity, $event_vars)
|
public function save($fields, $entity, $event_vars)
|
||||||
@ -72,7 +72,7 @@ class ActivityRepository extends BaseRepository
|
|||||||
/**
|
/**
|
||||||
* Creates a backup.
|
* Creates a backup.
|
||||||
*
|
*
|
||||||
* @param \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder $entity
|
* @param \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\Expense $entity
|
||||||
* @param \App\Models\Activity $activity The activity
|
* @param \App\Models\Activity $activity The activity
|
||||||
*/
|
*/
|
||||||
public function createBackup($entity, $activity)
|
public function createBackup($entity, $activity)
|
||||||
|
@ -126,6 +126,21 @@ class ClientRepository extends BaseRepository
|
|||||||
ClientFactory::create($user->company()->id, $user->id)
|
ClientFactory::create($user->company()->id, $user->id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk assign clients to a group.
|
||||||
|
*
|
||||||
|
* @param mixed $clients
|
||||||
|
* @param mixed $group_settings_id
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function assignGroup($clients, $group_settings_id): void
|
||||||
|
{
|
||||||
|
Client::query()
|
||||||
|
->company()
|
||||||
|
->whereIn('id', $clients->pluck('id'))
|
||||||
|
->update(['group_settings_id' => $group_settings_id]);
|
||||||
|
}
|
||||||
|
|
||||||
public function purge($client)
|
public function purge($client)
|
||||||
{
|
{
|
||||||
|
@ -46,10 +46,12 @@ class ExpenseRepository extends BaseRepository
|
|||||||
/** @var \App\Models\User $user */
|
/** @var \App\Models\User $user */
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
||||||
if(isset($data['payment_date']) && $data['payment_date'] == $expense->payment_date) {
|
$payment_date = &$data['payment_date'];
|
||||||
|
$vendor_id = &$data['vendor_id'];
|
||||||
|
|
||||||
|
if($payment_date && $payment_date == $expense->payment_date) {
|
||||||
//do nothing
|
//do nothing
|
||||||
} elseif(isset($data['payment_date']) && strlen($data['payment_date']) > 1 && $user->company()->notify_vendor_when_paid && (isset($data['vendor_id']) || $expense->vendor_id)) {
|
} elseif($payment_date && strlen($payment_date) > 1 && $user->company()->notify_vendor_when_paid && ($vendor_id || $expense->vendor_id)) {
|
||||||
nlog("ping");
|
|
||||||
$this->notify_vendor = true;
|
$this->notify_vendor = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +75,13 @@ class ExpenseRepository extends BaseRepository
|
|||||||
VendorExpenseNotify::dispatch($expense, $expense->company->db);
|
VendorExpenseNotify::dispatch($expense, $expense->company->db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($payment_date && strlen($payment_date) > 1 && $expense->purchase_order) {
|
||||||
|
$purchase_order = $expense->purchase_order;
|
||||||
|
$purchase_order->balance = round($purchase_order->amount - $expense->amount, 2);
|
||||||
|
$purchase_order->paid_to_date = $expense->amount;
|
||||||
|
$purchase_order->save();
|
||||||
|
}
|
||||||
|
|
||||||
return $expense;
|
return $expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,15 +118,60 @@ class SubscriptionRepository extends BaseRepository
|
|||||||
|
|
||||||
return $line_items;
|
return $line_items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConvertV3Bundle
|
||||||
|
*
|
||||||
|
* Removing the nested keys of the items array
|
||||||
|
*
|
||||||
|
* @param array $bundle
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function convertV3Bundle($bundle): array
|
||||||
|
{
|
||||||
|
if(is_object($bundle))
|
||||||
|
$bundle = json_decode(json_encode($bundle),1);
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach($bundle['recurring_products'] as $key => $value) {
|
||||||
|
|
||||||
|
$line_item = new \stdClass;
|
||||||
|
$line_item->product_key = $value['product']['product_key'];
|
||||||
|
$line_item->qty = (float) $value['quantity'];
|
||||||
|
$line_item->unit_cost = (float) $value['product']['price'];
|
||||||
|
$line_item->description = $value['product']['notes'];
|
||||||
|
$line_item->is_recurring = $value['product']['is_recurring'] ?? false;
|
||||||
|
$items[] = $line_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($bundle['recurring_products'] as $key => $value) {
|
||||||
|
|
||||||
|
$line_item = new \stdClass;
|
||||||
|
$line_item->product_key = $value['product']['product_key'];
|
||||||
|
$line_item->qty = (float) $value['quantity'];
|
||||||
|
$line_item->unit_cost = (float) $value['product']['price'];
|
||||||
|
$line_item->description = $value['product']['notes'];
|
||||||
|
$line_item->is_recurring = $value['product']['is_recurring'] ?? false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function generateBundleLineItems($bundle, $is_recurring = false, $is_credit = false)
|
public function generateBundleLineItems($bundle, $is_recurring = false, $is_credit = false)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
if(isset($bundle->recurring_products))
|
||||||
|
$bundle = $this->convertV3Bundle($bundle);
|
||||||
|
|
||||||
$multiplier = $is_credit ? -1 : 1;
|
$multiplier = $is_credit ? -1 : 1;
|
||||||
|
|
||||||
$line_items = [];
|
$line_items = [];
|
||||||
|
|
||||||
$line_items = collect($bundle)->filter(function ($item) {
|
$line_items = collect($bundle)->filter(function ($item) {
|
||||||
return $item->is_recurring;
|
return $item->is_recurring ?? false;
|
||||||
})->map(function ($item) {
|
})->map(function ($item) {
|
||||||
$line_item = new InvoiceItem();
|
$line_item = new InvoiceItem();
|
||||||
$line_item->product_key = $item->product_key;
|
$line_item->product_key = $item->product_key;
|
||||||
|
26
app/Rules/Subscriptions/Steps.php
Normal file
26
app/Rules/Subscriptions/Steps.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules\Subscriptions;
|
||||||
|
|
||||||
|
use App\Services\Subscription\StepService;
|
||||||
|
use Closure;
|
||||||
|
use App\Livewire\BillingPortal\Purchase;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
|
||||||
|
class Steps implements ValidationRule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the validation rule.
|
||||||
|
*
|
||||||
|
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||||
|
*/
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
$steps = StepService::mapToClassNames($value);
|
||||||
|
$errors = StepService::check($steps);
|
||||||
|
|
||||||
|
if (count($errors) > 0) {
|
||||||
|
$fail($errors[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -63,8 +63,10 @@ class Statement
|
|||||||
|
|
||||||
$variables = [];
|
$variables = [];
|
||||||
$variables = $html->generateLabelsAndValues();
|
$variables = $html->generateLabelsAndValues();
|
||||||
|
|
||||||
|
$option_template = &$this->options['template'];
|
||||||
|
|
||||||
if($this->client->getSetting('statement_design_id') != '') {
|
if($this->client->getSetting('statement_design_id') != '' || $option_template && $option_template != '') {
|
||||||
|
|
||||||
$variables['values']['$start_date'] = $this->translateDate($this->options['start_date'], $this->client->date_format(), $this->client->locale());
|
$variables['values']['$start_date'] = $this->translateDate($this->options['start_date'], $this->client->date_format(), $this->client->locale());
|
||||||
$variables['values']['$end_date'] = $this->translateDate($this->options['end_date'], $this->client->date_format(), $this->client->locale());
|
$variables['values']['$end_date'] = $this->translateDate($this->options['end_date'], $this->client->date_format(), $this->client->locale());
|
||||||
|
@ -44,6 +44,9 @@ class InstantPayment
|
|||||||
|
|
||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
|
nlog($this->request->all());
|
||||||
|
|
||||||
|
/** @var \App\Models\ClientContact $cc */
|
||||||
|
|
||||||
$cc = auth()->guard('contact')->user();
|
$cc = auth()->guard('contact')->user();
|
||||||
|
|
||||||
@ -69,6 +72,9 @@ class InstantPayment
|
|||||||
* ['invoice_id' => xxx, 'amount' => 22.00]
|
* ['invoice_id' => xxx, 'amount' => 22.00]
|
||||||
*/
|
*/
|
||||||
$payable_invoices = collect($this->request->payable_invoices);
|
$payable_invoices = collect($this->request->payable_invoices);
|
||||||
|
|
||||||
|
nlog($payable_invoices);
|
||||||
|
|
||||||
$invoices = Invoice::query()->whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
|
$invoices = Invoice::query()->whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
|
||||||
|
|
||||||
$invoices->each(function ($invoice) {
|
$invoices->each(function ($invoice) {
|
||||||
|
@ -43,6 +43,11 @@ class CreditService
|
|||||||
return (new CreateEDocument($this->credit))->handle();
|
return (new CreateEDocument($this->credit))->handle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEDocument($contact = null)
|
||||||
|
{
|
||||||
|
return $this->getECredit($contact);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the invoice number.
|
* Applies the invoice number.
|
||||||
* @return $this InvoiceService object
|
* @return $this InvoiceService object
|
||||||
|
119
app/Services/EDocument/Samples/ro.xml
Normal file
119
app/Services/EDocument/Samples/ro.xml
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||||
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
||||||
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||||
|
xmlns:ccts="urn:un:unece:uncefact:documentation:2"
|
||||||
|
xmlns:qdt="urn:oasis:names:specification:ubl:schema:xsd:QualifiedDataTypes-2"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:udt="urn:oasis:names:specification:ubl:schema:xsd:UnqualifiedDataTypes-2">
|
||||||
|
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||||
|
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1</cbc:CustomizationID>
|
||||||
|
<cbc:ID>ABC 0020</cbc:ID>
|
||||||
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
||||||
|
<cbc:DueDate>2024-01-15</cbc:DueDate>
|
||||||
|
<cbc:InvoiceTypeCode>384</cbc:InvoiceTypeCode>
|
||||||
|
<cbc:DocumentCurrencyCode>RON</cbc:DocumentCurrencyCode>
|
||||||
|
<cbc:TaxCurrencyCode>RON</cbc:TaxCurrencyCode>
|
||||||
|
<cac:AccountingSupplierParty>
|
||||||
|
<cac:Party>
|
||||||
|
<cac:PartyIdentification>
|
||||||
|
<cbc:ID>234234234</cbc:ID>
|
||||||
|
</cac:PartyIdentification>
|
||||||
|
<cac:PostalAddress>
|
||||||
|
<cbc:StreetName>This can be the full address , not just the street and street nr.</cbc:StreetName>
|
||||||
|
<cbc:CityName>SECTOR2</cbc:CityName>
|
||||||
|
<cbc:CountrySubentity>RO-B</cbc:CountrySubentity>
|
||||||
|
<cac:Country>
|
||||||
|
<cbc:IdentificationCode>RO</cbc:IdentificationCode>
|
||||||
|
</cac:Country>
|
||||||
|
</cac:PostalAddress>
|
||||||
|
<cac:PartyTaxScheme>
|
||||||
|
<cbc:CompanyID>RO234234234</cbc:CompanyID>
|
||||||
|
<cac:TaxScheme>
|
||||||
|
<cbc:ID>VAT</cbc:ID>
|
||||||
|
</cac:TaxScheme>
|
||||||
|
</cac:PartyTaxScheme>
|
||||||
|
<cac:PartyLegalEntity>
|
||||||
|
<cbc:RegistrationName>Some Copany Name</cbc:RegistrationName>
|
||||||
|
<cbc:CompanyID>J40/2222/2009</cbc:CompanyID>
|
||||||
|
</cac:PartyLegalEntity>
|
||||||
|
<cac:Contact>
|
||||||
|
<cbc:Name>Someone</cbc:Name>
|
||||||
|
<cbc:Telephone>88282819832</cbc:Telephone>
|
||||||
|
<cbc:ElectronicMail>some@email.com</cbc:ElectronicMail>
|
||||||
|
</cac:Contact>
|
||||||
|
</cac:Party>
|
||||||
|
</cac:AccountingSupplierParty>
|
||||||
|
<cac:AccountingCustomerParty>
|
||||||
|
<cac:Party>
|
||||||
|
<cac:PartyIdentification>
|
||||||
|
<cbc:ID>646546549</cbc:ID>
|
||||||
|
</cac:PartyIdentification>
|
||||||
|
<cac:PostalAddress>
|
||||||
|
<cbc:StreetName>This can be the full address , not just the street and street nr.</cbc:StreetName>
|
||||||
|
<cbc:CityName>SECTOR3</cbc:CityName>
|
||||||
|
<cbc:CountrySubentity>RO-B</cbc:CountrySubentity>
|
||||||
|
<cac:Country>
|
||||||
|
<cbc:IdentificationCode>RO</cbc:IdentificationCode>
|
||||||
|
</cac:Country>
|
||||||
|
</cac:PostalAddress>
|
||||||
|
<cac:PartyLegalEntity>
|
||||||
|
<cbc:RegistrationName>Some Comapny</cbc:RegistrationName>
|
||||||
|
<cbc:CompanyID>646546549</cbc:CompanyID>
|
||||||
|
</cac:PartyLegalEntity>
|
||||||
|
<cac:Contact>
|
||||||
|
<cbc:Name>Someone</cbc:Name>
|
||||||
|
<cbc:Telephone></cbc:Telephone>
|
||||||
|
<cbc:ElectronicMail>some@email.com</cbc:ElectronicMail>
|
||||||
|
</cac:Contact>
|
||||||
|
</cac:Party>
|
||||||
|
</cac:AccountingCustomerParty>
|
||||||
|
<cac:PaymentMeans>
|
||||||
|
<cbc:PaymentMeansCode>42</cbc:PaymentMeansCode>
|
||||||
|
<cac:PayeeFinancialAccount>
|
||||||
|
<cbc:ID>some account nr</cbc:ID>
|
||||||
|
<cbc:Name>Bank name</cbc:Name>
|
||||||
|
</cac:PayeeFinancialAccount>
|
||||||
|
</cac:PaymentMeans>
|
||||||
|
<cac:TaxTotal>
|
||||||
|
<cbc:TaxAmount currencyID="RON">63.65</cbc:TaxAmount>
|
||||||
|
<cac:TaxSubtotal>
|
||||||
|
<cbc:TaxableAmount currencyID="RON">335.00</cbc:TaxableAmount>
|
||||||
|
<cbc:TaxAmount currencyID="RON">63.65</cbc:TaxAmount>
|
||||||
|
<cac:TaxCategory>
|
||||||
|
<cbc:ID>S</cbc:ID> // this is a speciffic identifier for the VAT type <cbc:Percent>
|
||||||
|
19</cbc:Percent>
|
||||||
|
<cac:TaxScheme>
|
||||||
|
<cbc:ID>VAT</cbc:ID>
|
||||||
|
</cac:TaxScheme>
|
||||||
|
</cac:TaxCategory>
|
||||||
|
</cac:TaxSubtotal>
|
||||||
|
</cac:TaxTotal>
|
||||||
|
<cac:LegalMonetaryTotal>
|
||||||
|
<cbc:LineExtensionAmount currencyID="RON">335.00</cbc:LineExtensionAmount>
|
||||||
|
<cbc:TaxExclusiveAmount currencyID="RON">335.00</cbc:TaxExclusiveAmount>
|
||||||
|
<cbc:TaxInclusiveAmount currencyID="RON">398.65</cbc:TaxInclusiveAmount>
|
||||||
|
<cbc:AllowanceTotalAmount currencyID="RON">0.00</cbc:AllowanceTotalAmount>
|
||||||
|
<cbc:PayableAmount currencyID="RON">398.65</cbc:PayableAmount>
|
||||||
|
</cac:LegalMonetaryTotal>
|
||||||
|
<cac:InvoiceLine>
|
||||||
|
<cbc:ID>1</cbc:ID>
|
||||||
|
<cbc:InvoicedQuantity unitCode="H87">1</cbc:InvoicedQuantity> // unitcode
|
||||||
|
is a speciffic identifier for the type of product <cbc:LineExtensionAmount currencyID="RON">
|
||||||
|
335.00</cbc:LineExtensionAmount>
|
||||||
|
<cac:Item>
|
||||||
|
<cbc:Description>Some Description</cbc:Description>
|
||||||
|
<cbc:Name>Some product</cbc:Name>
|
||||||
|
<cac:ClassifiedTaxCategory>
|
||||||
|
<cbc:ID>S</cbc:ID> // this is a speciffic identifier for the VAT type <cbc:Percent>
|
||||||
|
19</cbc:Percent>
|
||||||
|
<cac:TaxScheme>
|
||||||
|
<cbc:ID>VAT</cbc:ID>
|
||||||
|
</cac:TaxScheme>
|
||||||
|
</cac:ClassifiedTaxCategory>
|
||||||
|
</cac:Item>
|
||||||
|
<cac:Price>
|
||||||
|
<cbc:PriceAmount currencyID="RON">335</cbc:PriceAmount>
|
||||||
|
</cac:Price>
|
||||||
|
</cac:InvoiceLine>
|
||||||
|
</Invoice>
|
@ -31,6 +31,7 @@ use CleverIt\UBL\Invoice\TaxCategory;
|
|||||||
use CleverIt\UBL\Invoice\TaxScheme;
|
use CleverIt\UBL\Invoice\TaxScheme;
|
||||||
use CleverIt\UBL\Invoice\TaxSubTotal;
|
use CleverIt\UBL\Invoice\TaxSubTotal;
|
||||||
use CleverIt\UBL\Invoice\TaxTotal;
|
use CleverIt\UBL\Invoice\TaxTotal;
|
||||||
|
use App\Models\Product;
|
||||||
|
|
||||||
class RoEInvoice extends AbstractService
|
class RoEInvoice extends AbstractService
|
||||||
{
|
{
|
||||||
@ -65,10 +66,10 @@ class RoEInvoice extends AbstractService
|
|||||||
$ubl_invoice = new UBLInvoice();
|
$ubl_invoice = new UBLInvoice();
|
||||||
|
|
||||||
// invoice
|
// invoice
|
||||||
$ubl_invoice->setId($invoice->custom_value1 . ' ' . $invoice->number);
|
$ubl_invoice->setId($invoice->number);
|
||||||
$ubl_invoice->setIssueDate(date_create($invoice->date));
|
$ubl_invoice->setIssueDate(date_create($invoice->date));
|
||||||
$ubl_invoice->setDueDate(date_create($invoice->due_date));
|
$ubl_invoice->setDueDate(date_create($invoice->due_date));
|
||||||
$ubl_invoice->setInvoiceTypeCode(explode('-', $invoice->custom_value3)[0]);
|
$ubl_invoice->setInvoiceTypeCode("380");
|
||||||
$ubl_invoice->setDocumentCurrencyCode($invoice->client->getCurrencyCode());
|
$ubl_invoice->setDocumentCurrencyCode($invoice->client->getCurrencyCode());
|
||||||
$ubl_invoice->setTaxCurrencyCode($invoice->client->getCurrencyCode());
|
$ubl_invoice->setTaxCurrencyCode($invoice->client->getCurrencyCode());
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ class RoEInvoice extends AbstractService
|
|||||||
->setTaxAmount($invoicing_data->getItemTotalTaxes())
|
->setTaxAmount($invoicing_data->getItemTotalTaxes())
|
||||||
->setTaxableAmount($taxable)
|
->setTaxableAmount($taxable)
|
||||||
->setTaxCategory((new TaxCategory())
|
->setTaxCategory((new TaxCategory())
|
||||||
->setId(explode('-', $company->settings->custom_value3)[0])
|
->setId("S")
|
||||||
->setPercent($taxRatePercent)
|
->setPercent($taxRatePercent)
|
||||||
->setTaxScheme(($taxNameScheme === 'TVA') ? 'VAT' : $taxNameScheme)));
|
->setTaxScheme(($taxNameScheme === 'TVA') ? 'VAT' : $taxNameScheme)));
|
||||||
$ubl_invoice->setTaxTotal($taxtotal);
|
$ubl_invoice->setTaxTotal($taxtotal);
|
||||||
@ -212,29 +213,29 @@ class RoEInvoice extends AbstractService
|
|||||||
{
|
{
|
||||||
if (strlen($item->tax_name1) > 1) {
|
if (strlen($item->tax_name1) > 1) {
|
||||||
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
||||||
->setId(explode('-', $item->custom_value4)[0])
|
->setId($this->resolveTaxCode($item->tax_id ?? 1))
|
||||||
->setPercent($item->tax_rate1)
|
->setPercent($item->tax_rate1)
|
||||||
->setTaxScheme(($item->tax_name1 === 'TVA') ? 'VAT' : $item->tax_name1);
|
->setTaxScheme(($item->tax_name1 === 'TVA') ? 'VAT' : $item->tax_name1);
|
||||||
} elseif (strlen($item->tax_name2) > 1) {
|
} elseif (strlen($item->tax_name2) > 1) {
|
||||||
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
||||||
->setId(explode('-', $item->custom_value4)[0])
|
->setId($this->resolveTaxCode($item->tax_id ?? 1))
|
||||||
->setPercent($item->tax_rate2)
|
->setPercent($item->tax_rate2)
|
||||||
->setTaxScheme(($item->tax_name2 === 'TVA') ? 'VAT' : $item->tax_name2);
|
->setTaxScheme(($item->tax_name2 === 'TVA') ? 'VAT' : $item->tax_name2);
|
||||||
} elseif (strlen($item->tax_name3) > 1) {
|
} elseif (strlen($item->tax_name3) > 1) {
|
||||||
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
||||||
->setId(explode('-', $item->custom_value4)[0])
|
->setId($this->resolveTaxCode($item->tax_id ?? 1))
|
||||||
->setPercent($item->tax_rate3)
|
->setPercent($item->tax_rate3)
|
||||||
->setTaxScheme(($item->tax_name3 === 'TVA') ? 'VAT' : $item->tax_name3);
|
->setTaxScheme(($item->tax_name3 === 'TVA') ? 'VAT' : $item->tax_name3);
|
||||||
}
|
}
|
||||||
$invoiceLine = (new InvoiceLine())
|
$invoiceLine = (new InvoiceLine())
|
||||||
->setId($index + 1)
|
->setId($index + 1)
|
||||||
->setInvoicedQuantity($item->quantity)
|
->setInvoicedQuantity($item->quantity)
|
||||||
->setUnitCode($item->custom_value3)
|
->setUnitCode($item->unit_code ?? 'C62')
|
||||||
->setLineExtensionAmount($item->line_total)
|
->setLineExtensionAmount($item->line_total)
|
||||||
->setItem((new Item())
|
->setItem((new Item())
|
||||||
->setName($item->product_key)
|
->setName($item->product_key)
|
||||||
->setDescription($item->notes)
|
->setDescription($item->notes)
|
||||||
->setClassifiedTaxCategory($classifiedTaxCategory))
|
->setClassifiedTaxCategory([$classifiedTaxCategory]))
|
||||||
->setPrice((new Price())
|
->setPrice((new Price())
|
||||||
->setPriceAmount($this->costWithDiscount($item)));
|
->setPriceAmount($this->costWithDiscount($item)));
|
||||||
|
|
||||||
@ -365,6 +366,26 @@ class RoEInvoice extends AbstractService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveTaxCode($tax_id)
|
||||||
|
{
|
||||||
|
$code = $tax_id;
|
||||||
|
|
||||||
|
match($tax_id){
|
||||||
|
Product::PRODUCT_TYPE_REVERSE_TAX => $code = 'AE', // VAT_REVERSE_CHARGE =
|
||||||
|
Product::PRODUCT_TYPE_EXEMPT => $code = 'E', // EXEMPT_FROM_TAX =
|
||||||
|
Product::PRODUCT_TYPE_PHYSICAL => $code = 'S', // STANDARD_RATE =
|
||||||
|
Product::PRODUCT_TYPE_ZERO_RATED => $code = 'Z', // ZERO_RATED_GOODS =
|
||||||
|
Product::PRODUCT_TYPE_REDUCED_TAX => $code = 'AA', // LOWER_RATE =
|
||||||
|
Product::PRODUCT_TYPE_SERVICE => $code = 'S', // STANDARD_RATE =
|
||||||
|
Product::PRODUCT_TYPE_DIGITAL => $code = 'S', // STANDARD_RATE =
|
||||||
|
Product::PRODUCT_TYPE_SHIPPING => $code = 'S', // STANDARD_RATE =
|
||||||
|
Product::PRODUCT_TYPE_OVERRIDE_TAX => $code = 'S', // STANDARD_RATE =
|
||||||
|
default => $code = 'S',
|
||||||
|
};
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
public function generateXml(): string
|
public function generateXml(): string
|
||||||
{
|
{
|
||||||
$ubl_invoice = $this->run(); // Call the existing handle method to get the UBLInvoice
|
$ubl_invoice = $this->run(); // Call the existing handle method to get the UBLInvoice
|
||||||
|
@ -93,7 +93,7 @@ class Email implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
public function backoff()
|
public function backoff()
|
||||||
{
|
{
|
||||||
return [rand(10, 20), rand(30, 45), rand(60, 79), rand(160, 400)];
|
return [rand(5, 29), rand(30, 59), rand(61, 100), rand(180, 500)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -314,6 +314,8 @@ class Email implements ShouldQueue
|
|||||||
$this->logMailError($e->getMessage(), $this->company->clients()->first());
|
$this->logMailError($e->getMessage(), $this->company->clients()->first());
|
||||||
$this->cleanUpMailers();
|
$this->cleanUpMailers();
|
||||||
|
|
||||||
|
$this->entityEmailFailed($message);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,6 +331,8 @@ class Email implements ShouldQueue
|
|||||||
$this->logMailError($message, $this->company->clients()->first());
|
$this->logMailError($message, $this->company->clients()->first());
|
||||||
$this->cleanUpMailers();
|
$this->cleanUpMailers();
|
||||||
|
|
||||||
|
$this->entityEmailFailed($message);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,11 +347,12 @@ class Email implements ShouldQueue
|
|||||||
|
|
||||||
if ($message_body && property_exists($message_body, 'Message')) {
|
if ($message_body && property_exists($message_body, 'Message')) {
|
||||||
$message = $message_body->Message;
|
$message = $message_body->Message;
|
||||||
nlog($message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->fail();
|
$this->fail();
|
||||||
|
$this->entityEmailFailed($message);
|
||||||
$this->cleanUpMailers();
|
$this->cleanUpMailers();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,7 +301,7 @@ class EmailDefaults
|
|||||||
$documents = [];
|
$documents = [];
|
||||||
|
|
||||||
/* Return early if the user cannot attach documents */
|
/* Return early if the user cannot attach documents */
|
||||||
if (!$this->email->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT) || $this->email->email_object->email_template_subject == 'email_subject_statement') {
|
if (!$this->email->email_object->invitation || !$this->email->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT) || $this->email->email_object->email_template_subject == 'email_subject_statement') {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,8 +320,8 @@ class EmailDefaults
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** E-Invoice xml file */
|
/** E-Invoice xml file */
|
||||||
if ($this->email->email_object->settings->enable_e_invoice && ! $this->email->email_object->entity instanceof PurchaseOrder) {
|
if ($this->email->email_object->settings->enable_e_invoice) {
|
||||||
$xml_string = $this->email->email_object->entity->service()->getEInvoice();
|
$xml_string = $this->email->email_object->entity->service()->getEDocument();
|
||||||
|
|
||||||
if($xml_string) {
|
if($xml_string) {
|
||||||
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($xml_string), 'name' => explode(".", $this->email->email_object->entity->getFileName('xml'))[0]."-e_invoice.xml"]]);
|
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($xml_string), 'name' => explode(".", $this->email->email_object->entity->getFileName('xml'))[0]."-e_invoice.xml"]]);
|
||||||
|
@ -49,7 +49,9 @@ class GenerateDeliveryNote
|
|||||||
if($design && $design->is_template) {
|
if($design && $design->is_template) {
|
||||||
|
|
||||||
$ts = new TemplateService($design);
|
$ts = new TemplateService($design);
|
||||||
$pdf = $ts->build([
|
|
||||||
|
$pdf = $ts->setCompany($this->invoice->company)
|
||||||
|
->build([
|
||||||
'invoices' => collect([$this->invoice]),
|
'invoices' => collect([$this->invoice]),
|
||||||
])->getPdf();
|
])->getPdf();
|
||||||
|
|
||||||
@ -62,8 +64,8 @@ class GenerateDeliveryNote
|
|||||||
: $this->decodePrimaryKey($this->invoice->client->getSetting('invoice_design_id'));
|
: $this->decodePrimaryKey($this->invoice->client->getSetting('invoice_design_id'));
|
||||||
|
|
||||||
$invitation = $this->invoice->invitations->first();
|
$invitation = $this->invoice->invitations->first();
|
||||||
// $file_path = sprintf('%s%s_delivery_note.pdf', $this->invoice->client->invoice_filepath($invitation), $this->invoice->number);
|
|
||||||
$file_path = sprintf('%sdelivery_note.pdf', $this->invoice->client->invoice_filepath($invitation));
|
// return (new \App\Services\Pdf\PdfService($invitation, 'delivery_note'))->boot()->getPdf();
|
||||||
|
|
||||||
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
|
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
|
||||||
return (new Phantom())->generate($this->invoice->invitations->first());
|
return (new Phantom())->generate($this->invoice->invitations->first());
|
||||||
@ -79,6 +81,9 @@ class GenerateDeliveryNote
|
|||||||
$template = new PdfMakerDesign(strtolower($design->name));
|
$template = new PdfMakerDesign(strtolower($design->name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$variables = $html->generateLabelsAndValues();
|
||||||
|
$variables['labels']['$entity_label']= ctrans('texts.delivery_note');
|
||||||
|
|
||||||
$state = [
|
$state = [
|
||||||
'template' => $template->elements([
|
'template' => $template->elements([
|
||||||
'client' => $this->invoice->client,
|
'client' => $this->invoice->client,
|
||||||
@ -86,7 +91,7 @@ class GenerateDeliveryNote
|
|||||||
'pdf_variables' => (array) $this->invoice->company->settings->pdf_variables,
|
'pdf_variables' => (array) $this->invoice->company->settings->pdf_variables,
|
||||||
'contact' => $this->contact,
|
'contact' => $this->contact,
|
||||||
], 'delivery_note'),
|
], 'delivery_note'),
|
||||||
'variables' => $html->generateLabelsAndValues(),
|
'variables' => $variables,
|
||||||
'options' => [
|
'options' => [
|
||||||
'client' => $this->invoice->client,
|
'client' => $this->invoice->client,
|
||||||
'entity' => $this->invoice,
|
'entity' => $this->invoice,
|
||||||
@ -111,12 +116,10 @@ class GenerateDeliveryNote
|
|||||||
info($maker->getCompiledHTML());
|
info($maker->getCompiledHTML());
|
||||||
}
|
}
|
||||||
|
|
||||||
return $pdf;
|
|
||||||
// Storage::disk($this->disk)->put($file_path, $pdf);
|
|
||||||
|
|
||||||
$maker = null;
|
$maker = null;
|
||||||
$state = null;
|
$state = null;
|
||||||
|
|
||||||
// return $file_path;
|
return $pdf;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,6 +204,11 @@ class InvoiceService
|
|||||||
return (new CreateEDocument($this->invoice))->handle();
|
return (new CreateEDocument($this->invoice))->handle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEDocument($contact = null)
|
||||||
|
{
|
||||||
|
return $this->getEInvoice($contact);
|
||||||
|
}
|
||||||
|
|
||||||
public function sendEmail($contact = null)
|
public function sendEmail($contact = null)
|
||||||
{
|
{
|
||||||
$send_email = new SendEmail($this->invoice, null, $contact);
|
$send_email = new SendEmail($this->invoice, null, $contact);
|
||||||
|
@ -141,7 +141,6 @@ class PdfMaker
|
|||||||
{
|
{
|
||||||
|
|
||||||
$html = $this->document->saveHTML();
|
$html = $this->document->saveHTML();
|
||||||
// nlog($html);
|
|
||||||
return str_replace('%24', '$', $html);
|
return str_replace('%24', '$', $html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,11 @@ class PurchaseOrderService
|
|||||||
{
|
{
|
||||||
return (new CreateEDocument($this->purchase_order))->handle();
|
return (new CreateEDocument($this->purchase_order))->handle();
|
||||||
}
|
}
|
||||||
|
public function getEDocument($contact = null)
|
||||||
|
{
|
||||||
|
return $this->getEPurchaseOrder($contact);
|
||||||
|
}
|
||||||
|
|
||||||
public function deleteEPurchaseOrder()
|
public function deleteEPurchaseOrder()
|
||||||
{
|
{
|
||||||
$this->purchase_order->load('invitations');
|
$this->purchase_order->load('invitations');
|
||||||
|
@ -78,6 +78,11 @@ class QuoteService
|
|||||||
return (new CreateEDocument($this->quote))->handle();
|
return (new CreateEDocument($this->quote))->handle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEDocument($contact = null)
|
||||||
|
{
|
||||||
|
return $this->getEQuote($contact);
|
||||||
|
}
|
||||||
|
|
||||||
public function sendEmail($contact = null): self
|
public function sendEmail($contact = null): self
|
||||||
{
|
{
|
||||||
$send_email = new SendEmail($this->quote, null, $contact);
|
$send_email = new SendEmail($this->quote, null, $contact);
|
||||||
|
70
app/Services/Subscription/StepService.php
Normal file
70
app/Services/Subscription/StepService.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services\Subscription;
|
||||||
|
|
||||||
|
use App\Livewire\BillingPortal\Purchase;
|
||||||
|
|
||||||
|
class StepService
|
||||||
|
{
|
||||||
|
public static function mapToClassNames(string $steps): array
|
||||||
|
{
|
||||||
|
$classes = collect(Purchase::$dependencies)->mapWithKeys(fn($dependency, $class) => [$dependency['id'] => $class])->toArray();
|
||||||
|
|
||||||
|
return array_map(fn($step) => $classes[$step], explode(',', $steps));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function check(array $steps): array
|
||||||
|
{
|
||||||
|
$dependencies = Purchase::$dependencies;
|
||||||
|
$step_order = array_flip($steps);
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($steps as $step) {
|
||||||
|
$dependent = $dependencies[$step]['dependencies'] ?? [];
|
||||||
|
|
||||||
|
if (!empty($dependent) && !array_intersect($dependent, $steps)) {
|
||||||
|
$errors[] = ctrans('texts.step_dependency_fail', [
|
||||||
|
'step' => ctrans('texts.' . self::mapClassNameToString($step)),
|
||||||
|
'dependencies' => implode(', ', array_map(fn($dependency) => ctrans('texts.' . self::mapClassNameToString($dependency)), $dependent)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($dependent as $dependency) {
|
||||||
|
if (in_array($dependency, $steps) && $step_order[$dependency] > $step_order[$step]) {
|
||||||
|
$errors[] = ctrans('texts.step_dependency_order_fail', [
|
||||||
|
'step' => ctrans('texts.' . self::mapClassNameToString($step)),
|
||||||
|
'dependency' => implode(', ', array_map(fn($dependency) => ctrans('texts.' . self::mapClassNameToString($dependency)), $dependent)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$auth = collect($dependencies)
|
||||||
|
->filter(fn ($dependency) => str_starts_with($dependency['id'], 'auth.'))
|
||||||
|
->keys()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
if (count(array_intersect($auth, $steps)) === 0) {
|
||||||
|
$errors[] = ctrans('texts.step_authentication_fail');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function mapClassNameToString(string $class): string
|
||||||
|
{
|
||||||
|
$classes = collect(Purchase::$dependencies)->mapWithKeys(fn($dependency, $class) => [$class => $dependency['id']])->toArray();
|
||||||
|
|
||||||
|
return $classes[$class];
|
||||||
|
}
|
||||||
|
}
|
204
app/Services/Subscription/SubscriptionCalculator.php
Normal file
204
app/Services/Subscription/SubscriptionCalculator.php
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services\Subscription;
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use App\DataMapper\InvoiceItem;
|
||||||
|
use App\Factory\InvoiceFactory;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use App\Helpers\Invoice\ProRata;
|
||||||
|
use App\Repositories\InvoiceRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubscriptionCalculator.
|
||||||
|
*/
|
||||||
|
class SubscriptionCalculator
|
||||||
|
{
|
||||||
|
use MakesHash;
|
||||||
|
|
||||||
|
public function __construct(public Subscription $subscription){}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildPurchaseInvoice
|
||||||
|
*
|
||||||
|
* @param array $context
|
||||||
|
* @return Invoice
|
||||||
|
*/
|
||||||
|
public function buildPurchaseInvoice(array $context): Invoice
|
||||||
|
{
|
||||||
|
|
||||||
|
$invoice_repo = new InvoiceRepository();
|
||||||
|
|
||||||
|
$invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
|
||||||
|
$invoice->subscription_id = $this->subscription->id;
|
||||||
|
$invoice->client_id = $this->decodePrimaryKey($context['client_id']);
|
||||||
|
$invoice->is_proforma = true;
|
||||||
|
$invoice->number = "####" . ctrans('texts.subscription') . "_" . now()->format('Y-m-d') . "_" . rand(0, 100000);
|
||||||
|
$invoice->line_items = $this->buildItems($context);
|
||||||
|
|
||||||
|
if(isset($context['valid_coupon']) && $context['valid_coupon']) {
|
||||||
|
$invoice->discount = $this->subscription->promo_discount;
|
||||||
|
$invoice->is_amount_discount = $this->subscription->is_amount_discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $invoice_repo->save([], $invoice);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Line Items
|
||||||
|
*
|
||||||
|
* @param array $context
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function buildItems(array $context): array
|
||||||
|
{
|
||||||
|
|
||||||
|
$bundle = $context['bundle'];
|
||||||
|
|
||||||
|
$recurring = array_merge(isset($bundle['recurring_products']) ? $bundle['recurring_products'] : [], isset($bundle['optional_recurring_products']) ? $bundle['optional_recurring_products'] : []);
|
||||||
|
$one_time = array_merge(isset($bundle['one_time_products']) ? $bundle['one_time_products'] : [], isset($bundle['optional_one_time_products']) ? $bundle['optional_one_time_products'] : []);
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach($recurring as $item) {
|
||||||
|
|
||||||
|
if($item['quantity'] < 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$line_item = new InvoiceItem();
|
||||||
|
$line_item->product_key = $item['product']['product_key'];
|
||||||
|
$line_item->quantity = (float) $item['quantity'];
|
||||||
|
$line_item->cost = (float) $item['product']['price'];
|
||||||
|
$line_item->notes = $item['product']['notes'];
|
||||||
|
$line_item->tax_id = $item['product']['tax_id'] ?? '1';
|
||||||
|
$items[] = $line_item;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($one_time as $item) {
|
||||||
|
|
||||||
|
if($item['quantity'] < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line_item = new InvoiceItem();
|
||||||
|
$line_item->product_key = $item['product']['product_key'];
|
||||||
|
$line_item->quantity = (float) $item['quantity'];
|
||||||
|
$line_item->cost = (float) $item['product']['price'];
|
||||||
|
$line_item->notes = $item['product']['notes'];
|
||||||
|
$line_item->tax_id = $item['product']['tax_id'] ?? '1';
|
||||||
|
$items[] = $line_item;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if the user is currently up
|
||||||
|
* to date with their payments for
|
||||||
|
* a given recurring invoice
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isPaidUp(Invoice $invoice): bool
|
||||||
|
{
|
||||||
|
$outstanding_invoices_exist = Invoice::query()->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||||
|
->where('subscription_id', $invoice->subscription_id)
|
||||||
|
->where('client_id', $invoice->client_id)
|
||||||
|
->where('balance', '>', 0)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
return ! $outstanding_invoices_exist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calcUpgradePlan(Invoice $invoice)
|
||||||
|
{
|
||||||
|
//set the starting refund amount
|
||||||
|
$refund_amount = 0;
|
||||||
|
|
||||||
|
$refund_invoice = false;
|
||||||
|
|
||||||
|
//are they paid up to date.
|
||||||
|
|
||||||
|
//yes - calculate refund
|
||||||
|
if ($this->isPaidUp($invoice)) {
|
||||||
|
$refund_invoice = $this->getRefundInvoice($invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($refund_invoice) {
|
||||||
|
/** @var \App\Models\Subscription $subscription **/
|
||||||
|
$subscription = Subscription::find($invoice->subscription_id);
|
||||||
|
$pro_rata = new ProRata();
|
||||||
|
|
||||||
|
$to_date = $subscription->service()->getNextDateForFrequency(Carbon::parse($refund_invoice->date), $subscription->frequency_id);
|
||||||
|
|
||||||
|
$refund_amount = $pro_rata->refund($refund_invoice->amount, now(), $to_date, $subscription->frequency_id);
|
||||||
|
|
||||||
|
$charge_amount = $pro_rata->charge($this->subscription->price, now(), $to_date, $this->subscription->frequency_id);
|
||||||
|
|
||||||
|
return $charge_amount - $refund_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
//no - return full freight charge.
|
||||||
|
return $this->subscription->price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function executeUpgradePlan() {}
|
||||||
|
|
||||||
|
private function getRefundInvoice(Invoice $invoice)
|
||||||
|
{
|
||||||
|
return Invoice::where('subscription_id', $invoice->subscription_id)
|
||||||
|
->where('client_id', $invoice->client_id)
|
||||||
|
->where('is_deleted', 0)
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
@ -11,33 +11,34 @@
|
|||||||
|
|
||||||
namespace App\Services\Template;
|
namespace App\Services\Template;
|
||||||
|
|
||||||
use App\Libraries\MultiDB;
|
use App\Models\Task;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Quote;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\Company;
|
|
||||||
use App\Models\Credit;
|
use App\Models\Credit;
|
||||||
use App\Models\Design;
|
use App\Models\Design;
|
||||||
|
use App\Models\Vendor;
|
||||||
|
use App\Models\Company;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
|
use App\Libraries\MultiDB;
|
||||||
use App\Models\PurchaseOrder;
|
use App\Models\PurchaseOrder;
|
||||||
use App\Models\Quote;
|
use Illuminate\Bus\Queueable;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
use App\Models\RecurringInvoice;
|
use App\Models\RecurringInvoice;
|
||||||
use App\Models\Task;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Vendor;
|
|
||||||
use App\Services\Email\AdminEmail;
|
use App\Services\Email\AdminEmail;
|
||||||
use App\Services\Email\EmailObject;
|
use App\Services\Email\EmailObject;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Services\PdfMaker\PdfMerge;
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Mail\Mailables\Address;
|
use Illuminate\Mail\Mailables\Address;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||||
|
|
||||||
class TemplateAction implements ShouldQueue
|
class TemplateAction implements ShouldQueue
|
||||||
{
|
{
|
||||||
@ -110,10 +111,44 @@ class TemplateAction implements ShouldQueue
|
|||||||
|
|
||||||
/** Set a global currency_code */
|
/** Set a global currency_code */
|
||||||
$first_entity = $result->first();
|
$first_entity = $result->first();
|
||||||
if($first_entity->client)
|
|
||||||
$currency_code = $first_entity->client->currency()->code;
|
/** Lets be clever and sniff out Statements */
|
||||||
elseif($first_entity instanceof Client)
|
if($first_entity instanceof Client && stripos(json_encode($template->design), '##statement##') !== false) {
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'show_payments_table' => true,
|
||||||
|
'show_aging_table' => true,
|
||||||
|
'status' => 'all',
|
||||||
|
'show_credits_table' => false,
|
||||||
|
'template' => $this->template,
|
||||||
|
];
|
||||||
|
|
||||||
|
$pdfs = [];
|
||||||
|
|
||||||
|
foreach($result as $client) {
|
||||||
|
$pdfs[] = $client->service()->statement($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(count($pdfs) == 1) {
|
||||||
|
$pdf = $pdfs[0];
|
||||||
|
} else {
|
||||||
|
$pdf = (new PdfMerge($pdfs))->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->send_email) {
|
||||||
|
$this->sendEmail($pdf, $template);
|
||||||
|
} else {
|
||||||
|
$filename = "templates/{$this->hash}.pdf";
|
||||||
|
Storage::disk(config('filesystems.default'))->put($filename, $pdf);
|
||||||
|
return $pdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if($first_entity instanceof Client)
|
||||||
$currency_code = $first_entity->currency()->code;
|
$currency_code = $first_entity->currency()->code;
|
||||||
|
elseif($first_entity->client)
|
||||||
|
$currency_code = $first_entity->client->currency()->code;
|
||||||
else
|
else
|
||||||
$currency_code = $this->company->currency()->code;
|
$currency_code = $this->company->currency()->code;
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -552,6 +552,7 @@ class TemplateService
|
|||||||
'payment_balance' => $invoice->client->payment_balance,
|
'payment_balance' => $invoice->client->payment_balance,
|
||||||
'credit_balance' => $invoice->client->credit_balance,
|
'credit_balance' => $invoice->client->credit_balance,
|
||||||
'vat_number' => $invoice->client->vat_number ?? '',
|
'vat_number' => $invoice->client->vat_number ?? '',
|
||||||
|
'currency' => $invoice->client->currency()->code ?? 'USD',
|
||||||
],
|
],
|
||||||
'payments' => $payments,
|
'payments' => $payments,
|
||||||
'total_tax_map' => $invoice->calc()->getTotalTaxMap(),
|
'total_tax_map' => $invoice->calc()->getTotalTaxMap(),
|
||||||
@ -655,7 +656,7 @@ class TemplateService
|
|||||||
'amount_raw' => $payment->amount,
|
'amount_raw' => $payment->amount,
|
||||||
'applied_raw' => $payment->applied,
|
'applied_raw' => $payment->applied,
|
||||||
'refunded_raw' => $payment->refunded,
|
'refunded_raw' => $payment->refunded,
|
||||||
'balance_raw' => ($payment->amount - $payment->refunded - $payment->applied),
|
'balance_raw' => ($payment->amount - $payment->applied),
|
||||||
'date' => $this->translateDate($payment->date, $payment->client->date_format(), $payment->client->locale()),
|
'date' => $this->translateDate($payment->date, $payment->client->date_format(), $payment->client->locale()),
|
||||||
'method' => $payment->translatedType(),
|
'method' => $payment->translatedType(),
|
||||||
'currency' => $payment->currency->code ?? $this->company->currency()->code,
|
'currency' => $payment->currency->code ?? $this->company->currency()->code,
|
||||||
@ -675,6 +676,7 @@ class TemplateService
|
|||||||
'payment_balance' => $payment->client->payment_balance,
|
'payment_balance' => $payment->client->payment_balance,
|
||||||
'credit_balance' => $payment->client->credit_balance,
|
'credit_balance' => $payment->client->credit_balance,
|
||||||
'vat_number' => $payment->client->vat_number ?? '',
|
'vat_number' => $payment->client->vat_number ?? '',
|
||||||
|
'currency' => $payment->client->currency()->code ?? 'USD',
|
||||||
],
|
],
|
||||||
'paymentables' => $pivot,
|
'paymentables' => $pivot,
|
||||||
'refund_activity' => $this->getPaymentRefundActivity($payment),
|
'refund_activity' => $this->getPaymentRefundActivity($payment),
|
||||||
@ -755,6 +757,7 @@ class TemplateService
|
|||||||
'payment_balance' => $quote->client->payment_balance,
|
'payment_balance' => $quote->client->payment_balance,
|
||||||
'credit_balance' => $quote->client->credit_balance,
|
'credit_balance' => $quote->client->credit_balance,
|
||||||
'vat_number' => $quote->client->vat_number ?? '',
|
'vat_number' => $quote->client->vat_number ?? '',
|
||||||
|
'currency' => $quote->client->currency()->code ?? 'USD',
|
||||||
],
|
],
|
||||||
'status_id' => $quote->status_id,
|
'status_id' => $quote->status_id,
|
||||||
'status' => Quote::stringStatus($quote->status_id),
|
'status' => Quote::stringStatus($quote->status_id),
|
||||||
@ -874,6 +877,7 @@ class TemplateService
|
|||||||
'payment_balance' => $credit->client->payment_balance,
|
'payment_balance' => $credit->client->payment_balance,
|
||||||
'credit_balance' => $credit->client->credit_balance,
|
'credit_balance' => $credit->client->credit_balance,
|
||||||
'vat_number' => $credit->client->vat_number ?? '',
|
'vat_number' => $credit->client->vat_number ?? '',
|
||||||
|
'currency' => $credit->client->currency()->code ?? 'USD',
|
||||||
],
|
],
|
||||||
'payments' => [],
|
'payments' => [],
|
||||||
'total_tax_map' => $credit->calc()->getTotalTaxMap(),
|
'total_tax_map' => $credit->calc()->getTotalTaxMap(),
|
||||||
@ -938,6 +942,7 @@ class TemplateService
|
|||||||
'payment_balance' => $task->client->payment_balance,
|
'payment_balance' => $task->client->payment_balance,
|
||||||
'credit_balance' => $task->client->credit_balance,
|
'credit_balance' => $task->client->credit_balance,
|
||||||
'vat_number' => $task->client->vat_number ?? '',
|
'vat_number' => $task->client->vat_number ?? '',
|
||||||
|
'currency' => $task->client->currency()->code ?? 'USD',
|
||||||
] : [],
|
] : [],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -999,6 +1004,7 @@ class TemplateService
|
|||||||
'payment_balance' => $project->client->payment_balance,
|
'payment_balance' => $project->client->payment_balance,
|
||||||
'credit_balance' => $project->client->credit_balance,
|
'credit_balance' => $project->client->credit_balance,
|
||||||
'vat_number' => $project->client->vat_number ?? '',
|
'vat_number' => $project->client->vat_number ?? '',
|
||||||
|
'currency' => $project->client->currency()->code ?? 'USD',
|
||||||
] : [],
|
] : [],
|
||||||
'user' => $this->userInfo($project->user)
|
'user' => $this->userInfo($project->user)
|
||||||
];
|
];
|
||||||
@ -1019,6 +1025,7 @@ class TemplateService
|
|||||||
'vendor' => $purchase_order->vendor ? [
|
'vendor' => $purchase_order->vendor ? [
|
||||||
'name' => $purchase_order->vendor->present()->name(),
|
'name' => $purchase_order->vendor->present()->name(),
|
||||||
'vat_number' => $purchase_order->vendor->vat_number ?? '',
|
'vat_number' => $purchase_order->vendor->vat_number ?? '',
|
||||||
|
'currency' => $purchase_order->vendor->currency()->code ?? 'USD',
|
||||||
] : [],
|
] : [],
|
||||||
'amount' => (float)$purchase_order->amount,
|
'amount' => (float)$purchase_order->amount,
|
||||||
'balance' => (float)$purchase_order->balance,
|
'balance' => (float)$purchase_order->balance,
|
||||||
@ -1030,6 +1037,7 @@ class TemplateService
|
|||||||
'vat_number' => $purchase_order->client->vat_number ?? '',
|
'vat_number' => $purchase_order->client->vat_number ?? '',
|
||||||
'address' => $purchase_order->client->present()->address(),
|
'address' => $purchase_order->client->present()->address(),
|
||||||
'shipping_address' => $purchase_order->client->present()->shipping_address(),
|
'shipping_address' => $purchase_order->client->present()->shipping_address(),
|
||||||
|
'currency' => $purchase_order->client->currency()->code ?? 'USD',
|
||||||
] : [],
|
] : [],
|
||||||
'status_id' => (string)($purchase_order->status_id ?: 1),
|
'status_id' => (string)($purchase_order->status_id ?: 1),
|
||||||
'status' => PurchaseOrder::stringStatus($purchase_order->status_id ?? 1),
|
'status' => PurchaseOrder::stringStatus($purchase_order->status_id ?? 1),
|
||||||
|
@ -72,6 +72,7 @@ class SubscriptionTransformer extends EntityTransformer
|
|||||||
'optional_recurring_product_ids' => (string)$subscription->optional_recurring_product_ids,
|
'optional_recurring_product_ids' => (string)$subscription->optional_recurring_product_ids,
|
||||||
'optional_product_ids' => (string) $subscription->optional_product_ids,
|
'optional_product_ids' => (string) $subscription->optional_product_ids,
|
||||||
'registration_required' => (bool) $subscription->registration_required,
|
'registration_required' => (bool) $subscription->registration_required,
|
||||||
|
'steps' => $subscription->steps,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@ class HtmlEngine
|
|||||||
$data['$from'] = ['value' => '', 'label' => ctrans('texts.from')];
|
$data['$from'] = ['value' => '', 'label' => ctrans('texts.from')];
|
||||||
$data['$to'] = ['value' => '', 'label' => ctrans('texts.to')];
|
$data['$to'] = ['value' => '', 'label' => ctrans('texts.to')];
|
||||||
$data['$shipping'] = ['value' => '', 'label' => ctrans('texts.ship_to')];
|
$data['$shipping'] = ['value' => '', 'label' => ctrans('texts.ship_to')];
|
||||||
|
$data['$ship_to'] = &$data['$shipping'];
|
||||||
$data['$total_tax_labels'] = ['value' => $this->totalTaxLabels(), 'label' => ctrans('texts.taxes')];
|
$data['$total_tax_labels'] = ['value' => $this->totalTaxLabels(), 'label' => ctrans('texts.taxes')];
|
||||||
$data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')];
|
$data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')];
|
||||||
$data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')];
|
$data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')];
|
||||||
@ -154,7 +154,6 @@ class HtmlEngine
|
|||||||
$data['$status_logo'] = ['value' => ' ', 'label' => ' '];
|
$data['$status_logo'] = ['value' => ' ', 'label' => ' '];
|
||||||
$data['$delivery_note'] = ['value' => ' ', 'label' => ctrans('texts.delivery_note')];
|
$data['$delivery_note'] = ['value' => ' ', 'label' => ctrans('texts.delivery_note')];
|
||||||
$data['$receipt'] = ['value' => ' ', 'label' => ctrans('texts.receipt')];
|
$data['$receipt'] = ['value' => ' ', 'label' => ctrans('texts.receipt')];
|
||||||
$data['$shipping'] = ['value' => ' ', 'label' => ctrans('texts.ship_to')];
|
|
||||||
|
|
||||||
$data['$invoice.date'] = &$data['$date'];
|
$data['$invoice.date'] = &$data['$date'];
|
||||||
$data['$invoiceDate'] = &$data['$date'];
|
$data['$invoiceDate'] = &$data['$date'];
|
||||||
@ -661,7 +660,6 @@ class HtmlEngine
|
|||||||
$data['$thanks'] = ['value' => '', 'label' => ctrans('texts.thanks')];
|
$data['$thanks'] = ['value' => '', 'label' => ctrans('texts.thanks')];
|
||||||
$data['$from'] = ['value' => '', 'label' => ctrans('texts.from')];
|
$data['$from'] = ['value' => '', 'label' => ctrans('texts.from')];
|
||||||
$data['$to'] = ['value' => '', 'label' => ctrans('texts.to')];
|
$data['$to'] = ['value' => '', 'label' => ctrans('texts.to')];
|
||||||
$data['$shipping'] = ['value' => '', 'label' => ctrans('texts.ship_to')];
|
|
||||||
|
|
||||||
$data['$details'] = ['value' => '', 'label' => ctrans('texts.details')];
|
$data['$details'] = ['value' => '', 'label' => ctrans('texts.details')];
|
||||||
|
|
||||||
|
@ -48,7 +48,11 @@ trait SubscriptionHooker
|
|||||||
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false,
|
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return array_merge($body, json_decode($response->getBody(), true));
|
if($response_body = json_decode($response->getBody(), true))
|
||||||
|
return array_merge($body, $response_body);
|
||||||
|
|
||||||
|
return array_merge($body, ['message' => 'Success', 'status_code' => 200]);
|
||||||
|
|
||||||
} catch (ClientException $e) {
|
} catch (ClientException $e) {
|
||||||
$message = $e->getMessage();
|
$message = $e->getMessage();
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ 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', 'invoicing.co'),
|
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
||||||
'app_version' => env('APP_VERSION', '5.8.39'),
|
'app_version' => env('APP_VERSION', '5.8.45'),
|
||||||
'app_tag' => env('APP_TAG', '5.8.39'),
|
'app_tag' => env('APP_TAG', '5.8.45'),
|
||||||
'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),
|
||||||
@ -102,7 +102,6 @@ return [
|
|||||||
'wepay' => env('WEPAY_KEYS', ''),
|
'wepay' => env('WEPAY_KEYS', ''),
|
||||||
'braintree' => env('BRAINTREE_KEYS', ''),
|
'braintree' => env('BRAINTREE_KEYS', ''),
|
||||||
'mollie' => env('MOLLIE_KEYS', ''),
|
'mollie' => env('MOLLIE_KEYS', ''),
|
||||||
'square' => env('SQUARE_KEYS', ''),
|
|
||||||
],
|
],
|
||||||
'contact' => [
|
'contact' => [
|
||||||
'email' => env('MAIL_FROM_ADDRESS'),
|
'email' => env('MAIL_FROM_ADDRESS'),
|
||||||
|
@ -27,6 +27,7 @@ class SubscriptionFactory extends Factory
|
|||||||
return [
|
return [
|
||||||
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
|
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
|
||||||
'name' => $this->faker->company(),
|
'name' => $this->faker->company(),
|
||||||
|
'steps' => "cart,auth.login-or-register",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Livewire\BillingPortal\Purchase;
|
||||||
|
use App\Services\Subscription\StepService;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('subscriptions', function (Blueprint $table) {
|
||||||
|
$table->string('steps')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$steps = collect(Purchase::defaultSteps())
|
||||||
|
->map(fn ($step) => StepService::mapClassNameToString($step))
|
||||||
|
->implode(',');
|
||||||
|
|
||||||
|
\App\Models\Subscription::query()
|
||||||
|
->withTrashed()
|
||||||
|
->cursor()
|
||||||
|
->each(function ($subscription) use ($steps){
|
||||||
|
|
||||||
|
$subscription->steps = $steps;
|
||||||
|
$subscription->save();
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -1173,8 +1173,8 @@ $lang = array(
|
|||||||
'invoice_number_padding' => 'Padding',
|
'invoice_number_padding' => 'Padding',
|
||||||
'preview' => 'Preview',
|
'preview' => 'Preview',
|
||||||
'list_vendors' => 'List Vendors',
|
'list_vendors' => 'List Vendors',
|
||||||
'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.',
|
'add_users_not_supported' => 'Upgrade to the Enterprise Plan to add additional users to your account.',
|
||||||
'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments, :link to see the full list of features.',
|
'enterprise_plan_features' => 'The Enterprise Plan adds support for multiple users and file attachments, :link to see the full list of features.',
|
||||||
'return_to_app' => 'Return To App',
|
'return_to_app' => 'Return To App',
|
||||||
|
|
||||||
|
|
||||||
@ -1323,7 +1323,7 @@ $lang = array(
|
|||||||
'security' => 'Security',
|
'security' => 'Security',
|
||||||
'see_whats_new' => 'See what\'s new in v:version',
|
'see_whats_new' => 'See what\'s new in v:version',
|
||||||
'wait_for_upload' => 'Please wait for the document upload to complete.',
|
'wait_for_upload' => 'Please wait for the document upload to complete.',
|
||||||
'upgrade_for_permissions' => 'Upgrade to our Enterprise plan to enable permissions.',
|
'upgrade_for_permissions' => 'Upgrade to our Enterprise Plan to enable permissions.',
|
||||||
'enable_second_tax_rate' => 'Enable specifying a <b>second tax rate</b>',
|
'enable_second_tax_rate' => 'Enable specifying a <b>second tax rate</b>',
|
||||||
'payment_file' => 'Payment File',
|
'payment_file' => 'Payment File',
|
||||||
'expense_file' => 'Expense File',
|
'expense_file' => 'Expense File',
|
||||||
@ -2699,7 +2699,7 @@ $lang = array(
|
|||||||
'no_assets' => 'No images, drag to upload',
|
'no_assets' => 'No images, drag to upload',
|
||||||
'add_image' => 'Add Image',
|
'add_image' => 'Add Image',
|
||||||
'select_image' => 'Select Image',
|
'select_image' => 'Select Image',
|
||||||
'upgrade_to_upload_images' => 'Upgrade to the enterprise plan to upload images',
|
'upgrade_to_upload_images' => 'Upgrade to the Enterprise Plan to upload images',
|
||||||
'delete_image' => 'Delete Image',
|
'delete_image' => 'Delete Image',
|
||||||
'delete_image_help' => 'Warning: deleting the image will remove it from all proposals.',
|
'delete_image_help' => 'Warning: deleting the image will remove it from all proposals.',
|
||||||
'amount_variable_help' => 'Note: the invoice $amount field will use the partial/deposit field if set otherwise it will use the invoice balance.',
|
'amount_variable_help' => 'Note: the invoice $amount field will use the partial/deposit field if set otherwise it will use the invoice balance.',
|
||||||
@ -3055,7 +3055,7 @@ $lang = array(
|
|||||||
'valid_until_days' => 'Valid Until',
|
'valid_until_days' => 'Valid Until',
|
||||||
'valid_until_days_help' => 'Automatically sets the <b>Valid Until</b> value on quotes to this many days in the future. Leave blank to disable.',
|
'valid_until_days_help' => 'Automatically sets the <b>Valid Until</b> value on quotes to this many days in the future. Leave blank to disable.',
|
||||||
'usually_pays_in_days' => 'Days',
|
'usually_pays_in_days' => 'Days',
|
||||||
'requires_an_enterprise_plan' => 'Requires an enterprise plan',
|
'requires_an_enterprise_plan' => 'Requires an Enterprise Plan',
|
||||||
'take_picture' => 'Take Picture',
|
'take_picture' => 'Take Picture',
|
||||||
'upload_file' => 'Upload File',
|
'upload_file' => 'Upload File',
|
||||||
'new_document' => 'New Document',
|
'new_document' => 'New Document',
|
||||||
@ -3157,7 +3157,7 @@ $lang = array(
|
|||||||
'archived_group' => 'Successfully archived group',
|
'archived_group' => 'Successfully archived group',
|
||||||
'deleted_group' => 'Successfully deleted group',
|
'deleted_group' => 'Successfully deleted group',
|
||||||
'restored_group' => 'Successfully restored group',
|
'restored_group' => 'Successfully restored group',
|
||||||
'upload_logo' => 'Upload Logo',
|
'upload_logo' => 'Upload Your Company Logo',
|
||||||
'uploaded_logo' => 'Successfully uploaded logo',
|
'uploaded_logo' => 'Successfully uploaded logo',
|
||||||
'saved_settings' => 'Successfully saved settings',
|
'saved_settings' => 'Successfully saved settings',
|
||||||
'device_settings' => 'Device Settings',
|
'device_settings' => 'Device Settings',
|
||||||
@ -3979,7 +3979,7 @@ $lang = array(
|
|||||||
'notification_credit_bounced_subject' => 'Unable to deliver Credit :invoice',
|
'notification_credit_bounced_subject' => 'Unable to deliver Credit :invoice',
|
||||||
'save_payment_method_details' => 'Save payment method details',
|
'save_payment_method_details' => 'Save payment method details',
|
||||||
'new_card' => 'New card',
|
'new_card' => 'New card',
|
||||||
'new_bank_account' => 'New bank account',
|
'new_bank_account' => 'Add Bank Account',
|
||||||
'company_limit_reached' => 'Limit of :limit companies per account.',
|
'company_limit_reached' => 'Limit of :limit companies per account.',
|
||||||
'credits_applied_validation' => 'Total credits applied cannot be MORE than total of invoices',
|
'credits_applied_validation' => 'Total credits applied cannot be MORE than total of invoices',
|
||||||
'credit_number_taken' => 'Credit number already taken',
|
'credit_number_taken' => 'Credit number already taken',
|
||||||
@ -5198,7 +5198,7 @@ $lang = array(
|
|||||||
'nordigen_handler_error_heading_account_config_invalid' => 'Missing Credentials',
|
'nordigen_handler_error_heading_account_config_invalid' => 'Missing Credentials',
|
||||||
'nordigen_handler_error_contents_account_config_invalid' => 'Invalid or missing credentials for Gocardless Bank Account Data. Contact support for help, if this issue persists.',
|
'nordigen_handler_error_contents_account_config_invalid' => 'Invalid or missing credentials for Gocardless Bank Account Data. Contact support for help, if this issue persists.',
|
||||||
'nordigen_handler_error_heading_not_available' => 'Not Available',
|
'nordigen_handler_error_heading_not_available' => 'Not Available',
|
||||||
'nordigen_handler_error_contents_not_available' => 'Feature unavailable, enterprise plan only.',
|
'nordigen_handler_error_contents_not_available' => 'Feature unavailable, Enterprise Plan only.',
|
||||||
'nordigen_handler_error_heading_institution_invalid' => 'Invalid Institution',
|
'nordigen_handler_error_heading_institution_invalid' => 'Invalid Institution',
|
||||||
'nordigen_handler_error_contents_institution_invalid' => 'The provided institution-id is invalid or no longer valid.',
|
'nordigen_handler_error_contents_institution_invalid' => 'The provided institution-id is invalid or no longer valid.',
|
||||||
'nordigen_handler_error_heading_ref_invalid' => 'Invalid Reference',
|
'nordigen_handler_error_heading_ref_invalid' => 'Invalid Reference',
|
||||||
@ -5244,11 +5244,27 @@ $lang = array(
|
|||||||
'user_sales' => 'User Sales',
|
'user_sales' => 'User Sales',
|
||||||
'iframe_url' => 'iFrame URL',
|
'iframe_url' => 'iFrame URL',
|
||||||
'user_unsubscribed' => 'User unsubscribed from emails :link',
|
'user_unsubscribed' => 'User unsubscribed from emails :link',
|
||||||
|
'out_of_stock' => 'Out of stock',
|
||||||
|
'step_dependency_fail' => 'Component ":step" requires at least one of it\'s dependencies (":dependencies") in the list.',
|
||||||
|
'step_dependency_order_fail' => 'Component ":step" depends on ":dependency". Make component(s) order is correct.',
|
||||||
|
'step_authentication_fail' => 'You must include at least one of authentication methods.',
|
||||||
|
'auth.login' => 'Login',
|
||||||
|
'auth.login-or-register' => 'Login or Register',
|
||||||
|
'auth.register' => 'Register',
|
||||||
|
'cart' => 'Cart',
|
||||||
|
'methods' => 'Methods',
|
||||||
|
'rff' => 'Required fields form',
|
||||||
|
'add_step' => 'Add step',
|
||||||
|
'steps' => 'Steps',
|
||||||
|
'steps_order_help' => 'The order of the steps is important. The first step should not depend on any other step. The second step should depend on the first step, and so on.',
|
||||||
|
'other_steps' => 'Other steps',
|
||||||
'use_available_payments' => 'Use Available Payments',
|
'use_available_payments' => 'Use Available Payments',
|
||||||
'test_email_sent' => 'Successfully sent email',
|
'test_email_sent' => 'Successfully sent email',
|
||||||
'gateway_type' => 'Gateway Type',
|
'gateway_type' => 'Gateway Type',
|
||||||
'save_template_body' => 'Would you like to save this import mapping as a template for future use?',
|
'save_template_body' => 'Would you like to save this import mapping as a template for future use?',
|
||||||
'save_as_template' => 'Save Template Mapping',
|
'save_as_template' => 'Save Template Mapping',
|
||||||
|
'checkout_only_for_existing_customers' => 'Checkout is enabled only for existing customers. Please login with existing account to checkout.',
|
||||||
|
'checkout_only_for_new_customers' => 'Checkout is enabled only for new customers. Please register a new account to checkout.',
|
||||||
'auto_bill_standard_invoices_help' => 'Auto bill standard invoices on the due date',
|
'auto_bill_standard_invoices_help' => 'Auto bill standard invoices on the due date',
|
||||||
'auto_bill_on_help' => 'Auto bill on send date OR due date (recurring invoices)',
|
'auto_bill_on_help' => 'Auto bill on send date OR due date (recurring invoices)',
|
||||||
'use_available_credits_help' => 'Apply any credit balances to payments prior to charging a payment method',
|
'use_available_credits_help' => 'Apply any credit balances to payments prior to charging a payment method',
|
||||||
@ -5270,9 +5286,11 @@ $lang = array(
|
|||||||
'enable_rappen_rounding_help' => 'Rounds totals to nearest 5',
|
'enable_rappen_rounding_help' => 'Rounds totals to nearest 5',
|
||||||
'duration_words' => 'Duration in words',
|
'duration_words' => 'Duration in words',
|
||||||
'upcoming_recurring_invoices' => 'Upcoming Recurring Invoices',
|
'upcoming_recurring_invoices' => 'Upcoming Recurring Invoices',
|
||||||
|
'shipping_country_id' => 'Shipping Country',
|
||||||
'show_table_footer' => 'Show table footer',
|
'show_table_footer' => 'Show table footer',
|
||||||
'show_table_footer_help' => 'Displays the totals in the footer of the table',
|
'show_table_footer_help' => 'Displays the totals in the footer of the table',
|
||||||
'total_invoices' => 'Total Invoices',
|
'total_invoices' => 'Total Invoices',
|
||||||
|
'add_to_group' => 'Add to group',
|
||||||
);
|
);
|
||||||
|
|
||||||
return $lang;
|
return $lang;
|
||||||
|
28
lang/en/ubl.php
Normal file
28
lang/en/ubl.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$lang = array(
|
||||||
|
'free_export_item' => 'Free export item',
|
||||||
|
'outside_tax_scope' => 'Outside tax scope',
|
||||||
|
'eea_goods_and_services' => 'EEA goods and services',
|
||||||
|
'lower_rate' => 'Lower rate',
|
||||||
|
'mixed_tax_rate' => 'Mixed tax rate',
|
||||||
|
'higher_rate' => 'Higher rate',
|
||||||
|
'canary_islands_indirect_tax' => 'Canary Islands indirect tax',
|
||||||
|
'ceuta_and_melilla' => 'Ceuta and Melilla',
|
||||||
|
'transferred_vat_italy' => 'Transferred VAT Italy',
|
||||||
|
'exempt_for_resale' => 'Exempt for resale',
|
||||||
|
'vat_not_now_due' => 'VAT not now due',
|
||||||
|
'vat_due_previous_invoice' => 'VAT due previous',
|
||||||
|
'transferred_vat' => 'Transferred VAT',
|
||||||
|
'duty_paid_by_supplier' => 'Duty paid by supplier',
|
||||||
|
'vat_margin_scheme_travel_agents' => 'VAT margin scheme travel agents',
|
||||||
|
'vat_margin_scheme_second_hand_goods' => 'VAT margin scheme second hand goods',
|
||||||
|
'vat_margin_scheme_works_of_art' => 'VAT margin scheme works of art',
|
||||||
|
'vat_margin_scheme_collectors_items' => 'VAT margin scheme collectors items',
|
||||||
|
'vat_exempt_eea_intra_community' => 'VAT exempt EEA intra community',
|
||||||
|
'canary_islands_tax' => 'Canary Islands tax',
|
||||||
|
'tax_ceuta_melilla' => 'Tax Ceuta Melilla',
|
||||||
|
'services_outside_scope' => 'Services outside scope',
|
||||||
|
);
|
||||||
|
|
||||||
|
return $lang;
|
File diff suppressed because one or more lines are too long
1
public/build/assets/app-91a05c24.css
vendored
1
public/build/assets/app-91a05c24.css
vendored
File diff suppressed because one or more lines are too long
1
public/build/assets/app-c6dc74fe.css
vendored
Normal file
1
public/build/assets/app-c6dc74fe.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -9,7 +9,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"resources/js/app.js": {
|
"resources/js/app.js": {
|
||||||
"file": "assets/app-c80ec97e.js",
|
"file": "assets/app-042e859e.js",
|
||||||
"imports": [
|
"imports": [
|
||||||
"_index-08e160a7.js",
|
"_index-08e160a7.js",
|
||||||
"__commonjsHelpers-725317a4.js"
|
"__commonjsHelpers-725317a4.js"
|
||||||
@ -240,7 +240,7 @@
|
|||||||
"src": "resources/js/setup/setup.js"
|
"src": "resources/js/setup/setup.js"
|
||||||
},
|
},
|
||||||
"resources/sass/app.scss": {
|
"resources/sass/app.scss": {
|
||||||
"file": "assets/app-91a05c24.css",
|
"file": "assets/app-c6dc74fe.css",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"src": "resources/sass/app.scss"
|
"src": "resources/sass/app.scss"
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
Livewire.on('beforePaymentEventsCompleted', () => {
|
Livewire.on('beforePaymentEventsCompleted', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('payment-method-form').submit()
|
document.getElementById('payment-method-form').submit()
|
||||||
}, 2000);
|
}, 2500);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
<div>
|
||||||
|
@if (session()->has('message'))
|
||||||
|
@component('portal.ninja2020.components.message')
|
||||||
|
{{ session('message') }}
|
||||||
|
@endcomponent
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<h1 class="text-3xl font-medium">{{ ctrans('texts.contact') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($state['initial_completed'] === false)
|
||||||
|
<form wire:submit="initial">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<label for="email_address">
|
||||||
|
<span class="input-label">{{ ctrans('texts.email_address') }}</span>
|
||||||
|
<input wire:model="email" type="email" class="input w-full" />
|
||||||
|
|
||||||
|
@error('email')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($state['login_form'])
|
||||||
|
<form wire:submit="handlePassword" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="input-label">{{ ctrans('texts.email_address') }}</span>
|
||||||
|
<input wire:model="email" type="email" class="input w-full" />
|
||||||
|
|
||||||
|
@error('email')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="input-label">{{ ctrans('texts.password') }}</span>
|
||||||
|
<input wire:model="password" type="password" class="input w-full" />
|
||||||
|
|
||||||
|
@error('password')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($state['otp_form'])
|
||||||
|
<form wire:submit="handleOtp" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="input-label">{{ ctrans('texts.code') }}</span>
|
||||||
|
<input wire:model="otp" type="text" class="input w-full" />
|
||||||
|
|
||||||
|
@error('otp')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
@ -0,0 +1,130 @@
|
|||||||
|
<form wire:submit="register(Object.fromEntries(new FormData($event.target)))" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="grid grid-cols-12 gap-4 mt-10">
|
||||||
|
@if($register_fields)
|
||||||
|
@foreach($register_fields as $field)
|
||||||
|
@if($field['visible'])
|
||||||
|
<div class="col-span-12 md:col-span-6">
|
||||||
|
<section class="flex items-center">
|
||||||
|
<label
|
||||||
|
for="{{ $field['key'] }}"
|
||||||
|
class="input-label">
|
||||||
|
@if(in_array($field['key'], ['custom_value1','custom_value2','custom_value3','custom_value4']))
|
||||||
|
{{ (new App\Utils\Helpers())->makeCustomField($subscription->company->custom_fields, str_replace("custom_value","client", $field['key']))}}
|
||||||
|
@elseif(array_key_exists('label', $field))
|
||||||
|
{{ ctrans("texts.{$field['label']}") }}
|
||||||
|
@else
|
||||||
|
{{ ctrans("texts.{$field['key']}") }}
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if($field['required'])
|
||||||
|
<section class="text-red-400 ml-1 text-sm">*</section>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@if($field['key'] === 'email')
|
||||||
|
<input
|
||||||
|
id="{{ $field['key'] }}"
|
||||||
|
class="input w-full"
|
||||||
|
type="email"
|
||||||
|
name="{{ $field['key'] }}"
|
||||||
|
value="{{ old($field['key'], $this->email ?? '') }}"
|
||||||
|
/>
|
||||||
|
@elseif($field['key'] === 'password')
|
||||||
|
<input
|
||||||
|
id="{{ $field['key'] }}"
|
||||||
|
class="input w-full"
|
||||||
|
type="password"
|
||||||
|
name="{{ $field['key'] }}"
|
||||||
|
/>
|
||||||
|
@elseif($field['key'] === 'currency_id')
|
||||||
|
<select
|
||||||
|
id="currency_id"
|
||||||
|
class="input w-full form-select bg-white"
|
||||||
|
name="currency_id">
|
||||||
|
@foreach(App\Utils\TranslationHelper::getCurrencies() as $currency)
|
||||||
|
<option
|
||||||
|
{{ $currency->id == $subscription->company->settings->currency_id ? 'selected' : null }} value="{{ $currency->id }}">
|
||||||
|
{{ $currency->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@elseif($field['key'] === 'country_id')
|
||||||
|
<select
|
||||||
|
id="shipping_country"
|
||||||
|
class="input w-full form-select bg-white"
|
||||||
|
name="country_id">
|
||||||
|
<option value="none"></option>
|
||||||
|
@foreach(App\Utils\TranslationHelper::getCountries() as $country)
|
||||||
|
<option
|
||||||
|
{{ $country == isset(auth()->user()->client->shipping_country->id) ? 'selected' : null }} value="{{ $country->id }}">
|
||||||
|
{{ $country->iso_3166_2 }}
|
||||||
|
({{ $country->name }})
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@elseif($field['key'] === 'shipping_country_id')
|
||||||
|
<select
|
||||||
|
id="shipping_country"
|
||||||
|
class="input w-full form-select bg-white"
|
||||||
|
name="shipping_country_id">
|
||||||
|
<option value="none"></option>
|
||||||
|
@foreach(App\Utils\TranslationHelper::getCountries() as $country)
|
||||||
|
<option
|
||||||
|
{{ $country == isset(auth()->user()->client->shipping_country->id) ? 'selected' : null }} value="{{ $country->id }}">
|
||||||
|
{{ $country->iso_3166_2 }}
|
||||||
|
({{ $country->name }})
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@else
|
||||||
|
<input
|
||||||
|
id="{{ $field['key'] }}"
|
||||||
|
class="input w-full"
|
||||||
|
name="{{ $field['key'] }}"
|
||||||
|
value="{{ old($field['key']) }}"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@error($field['key'])
|
||||||
|
<div class="validation validation-fail">
|
||||||
|
{{ $message }}
|
||||||
|
</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($field['key'] === 'password')
|
||||||
|
<div class="col-span-12 md:col-span-6">
|
||||||
|
<section class="flex items-center">
|
||||||
|
<label
|
||||||
|
for="password_confirmation"
|
||||||
|
class="input-label">
|
||||||
|
{{ ctrans('texts.password_confirmation') }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if($field['required'])
|
||||||
|
<section class="text-red-400 ml-1 text-sm">*</section>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="password_confirmation"
|
||||||
|
type="password"
|
||||||
|
class="input w-full"
|
||||||
|
name="password_confirmation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
@ -0,0 +1,95 @@
|
|||||||
|
<div>
|
||||||
|
@if (session()->has('message'))
|
||||||
|
@component('portal.ninja2020.components.message')
|
||||||
|
{{ session('message') }}
|
||||||
|
@endcomponent
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<h1 class="text-3xl font-medium">{{ ctrans('texts.contact') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($state['initial_completed'] === false)
|
||||||
|
<form wire:submit="initial">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<label for="email_address">
|
||||||
|
<span class="input-label">{{ ctrans('texts.email_address') }}</span>
|
||||||
|
<input wire:model="email" type="email" class="input w-full" />
|
||||||
|
|
||||||
|
@error('email')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($state['login_form'])
|
||||||
|
<form wire:submit="handlePassword" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="input-label">{{ ctrans('texts.email_address') }}</span>
|
||||||
|
<input wire:model="email" type="email" class="input w-full" />
|
||||||
|
|
||||||
|
@error('email')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="input-label">{{ ctrans('texts.password') }}</span>
|
||||||
|
<input wire:model="password" type="password" class="input w-full" />
|
||||||
|
|
||||||
|
@error('password')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($state['otp_form'])
|
||||||
|
<form wire:submit="handleOtp" class="space-y-3">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="input-label">{{ ctrans('texts.code') }}</span>
|
||||||
|
<input wire:model="otp" type="text" class="input w-full" />
|
||||||
|
|
||||||
|
@error('otp')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($state['register_form'])
|
||||||
|
@include('billing-portal.v3.authentication.register-form')
|
||||||
|
@endif
|
||||||
|
</div>
|
@ -0,0 +1,38 @@
|
|||||||
|
<div>
|
||||||
|
@if (session()->has('message'))
|
||||||
|
@component('portal.ninja2020.components.message')
|
||||||
|
{{ session('message') }}
|
||||||
|
@endcomponent
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<h1 class="text-3xl font-medium">{{ ctrans('texts.contact') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($state['initial_completed'] === false)
|
||||||
|
<form wire:submit="initial">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<label for="email_address">
|
||||||
|
<span class="input-label">{{ ctrans('texts.email_address') }}</span>
|
||||||
|
<input wire:model="email" type="email" class="input w-full" />
|
||||||
|
|
||||||
|
@error('email')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($state['register_form'])
|
||||||
|
@include('billing-portal.v3.authentication.register-form')
|
||||||
|
@endif
|
||||||
|
</div>
|
36
resources/views/billing-portal/v3/cart/cart.blade.php
Normal file
36
resources/views/billing-portal/v3/cart/cart.blade.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<div>
|
||||||
|
<livewire:billing-portal.cart.recurring-products
|
||||||
|
:subscription="$subscription"
|
||||||
|
:context="$context"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<livewire:billing-portal.cart.one-time-products
|
||||||
|
:subscription="$subscription"
|
||||||
|
:context="$context"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if($this->showOptionalProductsLabel())
|
||||||
|
<p class="text-xl mt-10 mb-4">{{ ctrans('texts.optional_products') }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<livewire:billing-portal.cart.optional-recurring-products
|
||||||
|
:subscription="$subscription"
|
||||||
|
:context="$context"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<livewire:billing-portal.cart.optional-one-time-products
|
||||||
|
:subscription="$subscription"
|
||||||
|
:context="$context"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<form wire:submit="handleSubmit">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4"
|
||||||
|
>
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,56 @@
|
|||||||
|
<div class="space-y-10">
|
||||||
|
@isset($context['bundle']['one_time_products'])
|
||||||
|
@foreach($context['bundle']['one_time_products'] as $key => $entry)
|
||||||
|
|
||||||
|
@php
|
||||||
|
$product = $entry['product'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-start justify-between space-x-4">
|
||||||
|
<div class="flex flex-start">
|
||||||
|
@if(filter_var($product['product_image'], FILTER_VALIDATE_URL))
|
||||||
|
<div
|
||||||
|
class="h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 mr-2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="{{ $product['product_image'] }}"
|
||||||
|
alt=""
|
||||||
|
class="h-full w-full object-cover object-center border rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h2 class="text-lg font-medium">{{ $product['product_key'] }}</h2>
|
||||||
|
<p class="block text-sm">{{ \App\Utils\Number::formatMoney($product['price'], $subscription['company']) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse space-y-3">
|
||||||
|
<div class="flex">
|
||||||
|
@if($subscription->per_seat_enabled)
|
||||||
|
@if($subscription->use_inventory_management && $product['in_stock_quantity'] <= 0)
|
||||||
|
<p class="text-sm font-light text-red-500 text-right mr-2 mt-2">{{ ctrans('texts.out_of_stock') }}</p>
|
||||||
|
@else
|
||||||
|
<p class="text-sm font-light text-gray-700 text-right mr-2 mt-2">{{ ctrans('texts.qty') }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<select id="{{ $product['hashed_id'] }}" wire:change="quantity($event.target.id, $event.target.value)" class="rounded-md border-gray-300 shadow-sm sm:text-sm" {{ $subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}>
|
||||||
|
<option {{ $entry['quantity'] == '0' ? 'selected' : '' }} value="0" selected="selected">0</option>
|
||||||
|
@for ($i = 1; $i <= ($subscription->use_inventory_management ? min($product['in_stock_quantity'], min(100,$product['max_quantity'])) : min(100,$product['max_quantity'])); $i++)
|
||||||
|
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="prose my-3 text-sm">
|
||||||
|
{!! \App\Models\Product::markdownHelp($product['notes']) !!}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endisset
|
||||||
|
</div>
|
@ -0,0 +1,54 @@
|
|||||||
|
<div class="space-y-10">
|
||||||
|
@isset($context['bundle']['optional_one_time_products'])
|
||||||
|
@foreach($context['bundle']['optional_one_time_products'] as $key => $entry)
|
||||||
|
|
||||||
|
@php
|
||||||
|
$product = $entry['product'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-start justify-between space-x-4">
|
||||||
|
<div class="flex flex-start">
|
||||||
|
@if(filter_var($product['product_image'], FILTER_VALIDATE_URL))
|
||||||
|
<div
|
||||||
|
class="h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 mr-2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="{{ $product['product_image'] }}"
|
||||||
|
alt=""
|
||||||
|
class="h-full w-full object-cover object-center border rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h2 class="text-lg font-medium">{{ $product['product_key'] }}</h2>
|
||||||
|
<p class="block text-sm">{{ \App\Utils\Number::formatMoney($product['price'], $subscription['company']) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse space-y-3">
|
||||||
|
<div class="flex">
|
||||||
|
@if($subscription->use_inventory_management && $product['in_stock_quantity'] <= 0)
|
||||||
|
<p class="text-sm font-light text-red-500 text-right mr-2 mt-2">{{ ctrans('texts.out_of_stock') }}</p>
|
||||||
|
@else
|
||||||
|
<p class="text-sm font-light text-gray-700 text-right mr-2 mt-2">{{ ctrans('texts.qty') }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<select id="{{ $product['hashed_id'] }}" wire:change="quantity($event.target.id, $event.target.value)" class="rounded-md border-gray-300 shadow-sm sm:text-sm" {{ $subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}>
|
||||||
|
<option {{ $entry['quantity'] == '0' ? 'selected' : '' }} value="0" selected="selected">0</option>
|
||||||
|
@for ($i = 1; $i <= ($subscription->use_inventory_management ? min($product['in_stock_quantity'], min(100,$product['max_quantity'])) : min(100,$product['max_quantity'])); $i++)
|
||||||
|
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="prose my-3 text-sm">
|
||||||
|
{!! \App\Models\Product::markdownHelp($product['notes']) !!}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endisset
|
||||||
|
</div>
|
@ -0,0 +1,56 @@
|
|||||||
|
<div class="space-y-10">
|
||||||
|
@isset($context['bundle']['optional_recurring_products'])
|
||||||
|
@foreach($context['bundle']['optional_recurring_products'] as $key => $entry)
|
||||||
|
|
||||||
|
@php
|
||||||
|
$product = $entry['product'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-start justify-between space-x-4">
|
||||||
|
<div class="flex flex-start">
|
||||||
|
@if(filter_var($product['product_image'], FILTER_VALIDATE_URL))
|
||||||
|
<div
|
||||||
|
class="h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 mr-2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="{{ $product['product_image'] }}"
|
||||||
|
alt=""
|
||||||
|
class="h-full w-full object-cover object-center border rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h2 class="text-lg font-medium">{{ $product['product_key'] }}</h2>
|
||||||
|
<p class="block text-sm">{{ \App\Utils\Number::formatMoney($product['price'], $subscription['company']) }} / <span class="lowercase">{{ App\Models\RecurringInvoice::frequencyForKey($subscription->frequency_id) }}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse space-y-3">
|
||||||
|
<div class="flex">
|
||||||
|
|
||||||
|
@if($subscription->use_inventory_management && $product['in_stock_quantity'] <= 0)
|
||||||
|
<p class="text-sm font-light text-red-500 text-right mr-2 mt-2">{{ ctrans('texts.out_of_stock') }}</p>
|
||||||
|
@else
|
||||||
|
<p class="text-sm font-light text-gray-700 text-right mr-2 mt-2">{{ ctrans('texts.qty') }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<select id="{{ $product['hashed_id'] }}" wire:change="quantity($event.target.id, $event.target.value)" class="rounded-md border-gray-300 shadow-sm sm:text-sm" {{ $subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}>
|
||||||
|
<option {{ $entry['quantity'] == '0' ? 'selected' : '' }} value="0" selected="selected">0</option>
|
||||||
|
@for ($i = 1; $i <= ($subscription->use_inventory_management ? min($product['in_stock_quantity'], min(100,$product['max_quantity'])) : min(100,$product['max_quantity'])); $i++)
|
||||||
|
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="prose my-3 text-sm">
|
||||||
|
{!! \App\Models\Product::markdownHelp($product['notes']) !!}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endisset
|
||||||
|
</div>
|
@ -0,0 +1,68 @@
|
|||||||
|
<div class="space-y-10">
|
||||||
|
@isset($context['bundle']['recurring_products'])
|
||||||
|
@foreach($context['bundle']['recurring_products'] as $key => $entry)
|
||||||
|
|
||||||
|
@php
|
||||||
|
$product = $entry['product'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-start justify-between space-x-4">
|
||||||
|
<div class="flex flex-start">
|
||||||
|
@if(filter_var($product['product_image'], FILTER_VALIDATE_URL))
|
||||||
|
<div
|
||||||
|
class="h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 mr-2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="{{ $product['product_image'] }}"
|
||||||
|
alt=""
|
||||||
|
class="h-full w-full object-cover object-center border rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h2 class="text-lg font-medium">{{ $product['product_key'] }}</h2>
|
||||||
|
<p class="block text-sm">{{ \App\Utils\Number::formatMoney($product['price'], $subscription['company']) }} / <span class="lowercase">{{ App\Models\RecurringInvoice::frequencyForKey($subscription->frequency_id) }}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse space-y-3">
|
||||||
|
<div class="flex">
|
||||||
|
@if($subscription->per_seat_enabled)
|
||||||
|
@if($subscription->use_inventory_management && $product['in_stock_quantity'] < 1)
|
||||||
|
<p class="text-sm font-light text-red-500 text-right mr-2 mt-2">{{ ctrans('texts.out_of_stock') }}</p>
|
||||||
|
@else
|
||||||
|
<p class="text-sm font-light text-gray-700 text-right mr-2 mt-2">{{ ctrans('texts.qty') }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<select
|
||||||
|
id="{{ $product['hashed_id'] }}"
|
||||||
|
class="rounded-md border-gray-300 shadow-sm sm:text-sm"
|
||||||
|
wire:change="quantity($event.target.id, $event.target.value)"
|
||||||
|
{{ $subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}
|
||||||
|
>
|
||||||
|
<option {{ $entry['quantity'] == '1' ? 'selected' : '' }} value="1">1</option>
|
||||||
|
|
||||||
|
@if($subscription->max_seats_limit > 1)
|
||||||
|
@for ($i = 2; $i <= ($subscription->use_inventory_management ? min($subscription->max_seats_limit,$product['in_stock_quantity']) : $subscription->max_seats_limit); $i++)
|
||||||
|
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
|
||||||
|
@endfor
|
||||||
|
@else
|
||||||
|
@for ($i = 2; $i <= ($subscription->use_inventory_management ? min($product['in_stock_quantity'], min(100,$product['max_quantity'])) : min(100,$product['max_quantity'])); $i++)
|
||||||
|
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
|
||||||
|
@endfor
|
||||||
|
@endif
|
||||||
|
</select>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="prose my-3 text-sm">
|
||||||
|
{!! \App\Models\Product::markdownHelp($product['notes']) !!}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endisset
|
||||||
|
</div>
|
46
resources/views/billing-portal/v3/index.blade.php
Normal file
46
resources/views/billing-portal/v3/index.blade.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
@extends('portal.ninja2020.layout.clean')
|
||||||
|
@section('meta_title', ctrans('texts.purchase'))
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger" style="margin: 1rem">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<p>{{ $error }}</p>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@livewire('billing-portal.purchase', ['subscription' => $subscription, 'db' => $subscription->company->db, 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null])
|
||||||
|
@stop
|
||||||
|
|
||||||
|
@push('footer')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('livewire:init', () => {
|
||||||
|
Livewire.on('purchase.submit', (event) => {
|
||||||
|
document.getElementById('payment-method-form').submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
const target = document.getElementById('container');
|
||||||
|
|
||||||
|
Livewire.on('purchase.next', (event) => {
|
||||||
|
document.getElementById('spinner').classList.remove('hidden');
|
||||||
|
document.getElementById('container').classList.add('hidden');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('spinner').classList.add('hidden');
|
||||||
|
document.getElementById('container').classList.remove('hidden');
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
Livewire.on('update-shipping-data', (event) => {
|
||||||
|
for (field in event) {
|
||||||
|
let element = document.querySelector(`input[name=${field}]`);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.value = event[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
11
resources/views/billing-portal/v3/payments/methods.blade.php
Normal file
11
resources/views/billing-portal/v3/payments/methods.blade.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<div>
|
||||||
|
<h1 class="text-2xl">{{ ctrans('texts.payment_methods') }}</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-3 my-3">
|
||||||
|
@foreach($methods as $method)
|
||||||
|
<button class="flex items-center justify-between mb-4 bg-white rounded px-6 py-4 shadow-sm border" wire:click="handleSelect('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}'); $wire.$refresh();">
|
||||||
|
{{ $method['label'] }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
52
resources/views/billing-portal/v3/purchase.blade.php
Normal file
52
resources/views/billing-portal/v3/purchase.blade.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<div class="grid grid-cols-12 bg-gray-50">
|
||||||
|
<div
|
||||||
|
@php
|
||||||
|
nlog($context);
|
||||||
|
@endphp
|
||||||
|
class="col-span-12 xl:col-span-6 bg-white flex flex-col items-center lg:h-screen"
|
||||||
|
>
|
||||||
|
<div class="w-full p-10 lg:mt-24 md:max-w-xl">
|
||||||
|
<img
|
||||||
|
class="h-8"
|
||||||
|
src="{{ $subscription->company->present()->logo }}"
|
||||||
|
alt="{{ $subscription->company->present()->name }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<svg id="spinner" class="animate-spin h-8 w-8 text-primary mt-10 hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="my-10" id="container">
|
||||||
|
@livewire($this->component, ['context' => $context, 'subscription' => $this->subscription], key($this->componentUniqueId()))
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-12 xl:col-span-6">
|
||||||
|
<div class="sticky top-0">
|
||||||
|
<div class="w-full p-10 lg:mt-24 md:max-w-xl">
|
||||||
|
<div class="my-6 space-y-10 xl:ml-5">
|
||||||
|
@livewire('billing-portal.summary', ['subscription' => $subscription, 'context' => $context], key($this->summaryUniqueId()))
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
action="{{ route('client.payments.process', ['hash' => $hash, 'sidebar' => 'hidden', 'source' => 'subscriptions']) }}"
|
||||||
|
method="post"
|
||||||
|
id="payment-method-form">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<input type="hidden" name="action" value="payment">
|
||||||
|
<input type="hidden" name="invoices[]" />
|
||||||
|
<input type="hidden" name="payable_invoices[0][amount]" value="{{ $this->context['form']['payable_amount'] ?? '' }}" />
|
||||||
|
<input type="hidden" name="payable_invoices[0][invoice_id]" value="{{ $this->context['form']['invoice_hashed_id'] ?? '' }}" />
|
||||||
|
<input type="hidden" name="company_gateway_id" value="{{ $this->context['form']['company_gateway_id'] ?? '' }}" />
|
||||||
|
<input type="hidden" name="payment_method_id" value="{{ $this->context['form']['payment_method_id'] ?? '' }}" />
|
||||||
|
<input type="hidden" name="contact_first_name" value="{{ $this->context['contact']['first_name'] ?? '' }}" />
|
||||||
|
<input type="hidden" name="contact_last_name" value="{{ $this->context['contact']['last_name'] ?? '' }}" />
|
||||||
|
<input type="hidden" name="contact_email" value="{{ $this->context['contact']['email'] ?? '' }}" />
|
||||||
|
</form>
|
||||||
|
</div>
|
46
resources/views/billing-portal/v3/rff-basic.blade.php
Normal file
46
resources/views/billing-portal/v3/rff-basic.blade.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<form wire:submit="handleSubmit">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<label for="contact_first_name">
|
||||||
|
<span class="input-label">{{ ctrans('texts.first_name') }}</span>
|
||||||
|
<input wire:model="contact_first_name" type="text" class="input w-full" />
|
||||||
|
|
||||||
|
@error('contact_first_name')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="contact_last_name">
|
||||||
|
<span class="input-label">{{ ctrans('texts.last_name') }}</span>
|
||||||
|
<input wire:model="contact_last_name" type="text" class="input w-full" />
|
||||||
|
|
||||||
|
@error('contact_last_name')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="contact_email">
|
||||||
|
<span class="input-label">{{ ctrans('texts.email_address') }}</span>
|
||||||
|
<input wire:model="contact_email" type="email" class="input w-full" />
|
||||||
|
|
||||||
|
@error('contact_email')
|
||||||
|
<p class="validation validation-fail block w-full" role="alert">
|
||||||
|
{{ $message }}
|
||||||
|
</p>
|
||||||
|
@enderror
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
15
resources/views/billing-portal/v3/rff.blade.php
Normal file
15
resources/views/billing-portal/v3/rff.blade.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<div>
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<ul>
|
||||||
|
@foreach($errors->all() as $error)
|
||||||
|
<li class="text-sm">{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div>
|
||||||
|
@livewire('required-client-info', ['db' => $company->db, 'fields' => method_exists($gateway, 'getClientRequiredFields') ? $gateway->getClientRequiredFields() : [], 'contact_id' => auth()->guard('contact')->user()->id, 'countries' => $countries, 'company_id' => $company->id, 'company_gateway_id' => $gateway->company_gateway ? $gateway->company_gateway->id : $gateway->id, 'form_only' => true])
|
||||||
|
</div>
|
||||||
|
</div>
|
35
resources/views/billing-portal/v3/summary.blade.php
Normal file
35
resources/views/billing-portal/v3/summary.blade.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<div class="space-y-4">
|
||||||
|
<h1 class="text-2xl">{{ ctrans('texts.order') }}</h1>
|
||||||
|
|
||||||
|
@isset($this->context['bundle'])
|
||||||
|
<div class="space-y-2">
|
||||||
|
@foreach($this->items() as $item)
|
||||||
|
@if($item['quantity'] > 0)
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span>{{ $item['quantity'] }}x {{ $item['product_key'] }}</span>
|
||||||
|
<span>{{ $item['total'] }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 mt-4 border-t pt-2">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="uppercase">{{ ctrans('texts.one_time_purchases') }}</span>
|
||||||
|
<span>{{ $this->oneTimePurchasesTotal() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="uppercase">{{ ctrans('texts.recurring_purchases') }}</span>
|
||||||
|
<span>{{ $this->recurringPurchasesTotal() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex justify-between text-sm uppercase border-t pt-2"
|
||||||
|
>
|
||||||
|
<span>{{ ctrans('texts.total') }}</span>
|
||||||
|
<span class="font-semibold">{{ $this->total() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user