diff --git a/VERSION.txt b/VERSION.txt index fd2784a8431e..6ca7fd49ab1d 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.5.49 \ No newline at end of file +5.5.50 \ No newline at end of file diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index cc236aac364d..6033dabcdc32 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -119,7 +119,8 @@ class CheckData extends Command $this->checkDuplicateRecurringInvoices(); $this->checkOauthSanity(); $this->checkVendorSettings(); - + $this->checkClientSettings(); + if(Ninja::isHosted()){ $this->checkAccountStatuses(); $this->checkNinjaPortalUrls(); @@ -952,24 +953,24 @@ class CheckData extends Command if ($this->option('fix') == 'true') { - Client::query()->whereNull('settings->currency_id')->cursor()->each(function ($client){ + // Client::query()->whereNull('settings->currency_id')->cursor()->each(function ($client){ - if(is_array($client->settings) && count($client->settings) == 0) - { - $settings = ClientSettings::defaults(); - $settings->currency_id = $client->company->settings->currency_id; - } - else { - $settings = $client->settings; - $settings->currency_id = $client->company->settings->currency_id; - } + // if(is_array($client->settings) && count($client->settings) == 0) + // { + // $settings = ClientSettings::defaults(); + // $settings->currency_id = $client->company->settings->currency_id; + // } + // else { + // $settings = $client->settings; + // $settings->currency_id = $client->company->settings->currency_id; + // } - $client->settings = $settings; - $client->save(); + // $client->settings = $settings; + // $client->save(); - $this->logMessage("Fixing currency for # {$client->id}"); + // $this->logMessage("Fixing currency for # {$client->id}"); - }); + // }); Client::query()->whereNull('country_id')->cursor()->each(function ($client){ diff --git a/app/Console/Commands/CreateSingleAccount.php b/app/Console/Commands/CreateSingleAccount.php index 147ae4ad516a..2d6c983ae043 100644 --- a/app/Console/Commands/CreateSingleAccount.php +++ b/app/Console/Commands/CreateSingleAccount.php @@ -307,7 +307,7 @@ class CreateSingleAccount extends Command $webhook_config = [ 'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan', 'post_purchase_rest_method' => 'POST', - 'post_purchase_headers' => [], + 'post_purchase_headers' => [config('ninja.ninja_hosted_header') => config('ninja.ninja_hosted_secret')], ]; $sub = SubscriptionFactory::create($company->id, $user->id); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 9beaefda8822..3c5fd83d61ff 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -23,6 +23,7 @@ use App\Jobs\Ninja\QueueSize; use App\Jobs\Ninja\SystemMaintenance; use App\Jobs\Ninja\TaskScheduler; use App\Jobs\Quote\QuoteCheckExpired; +use App\Jobs\Subscription\CleanStaleInvoiceOrder; use App\Jobs\Util\DiskCleanup; use App\Jobs\Util\ReminderJob; use App\Jobs\Util\SchedulerCheck; @@ -68,6 +69,9 @@ class Kernel extends ConsoleKernel /* Sends recurring invoices*/ $schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer(); + /* Stale Invoice Cleanup*/ + $schedule->job(new CleanStaleInvoiceOrder)->hourly()->withoutOverlapping()->name('stale-invoice-job')->onOneServer(); + /* Sends recurring invoices*/ $schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer(); diff --git a/app/DataMapper/ClientRegistrationFields.php b/app/DataMapper/ClientRegistrationFields.php index 5e6397267dfe..505e40742490 100644 --- a/app/DataMapper/ClientRegistrationFields.php +++ b/app/DataMapper/ClientRegistrationFields.php @@ -93,6 +93,10 @@ class ClientRegistrationFields 'key' => 'vat_number', 'required' => false, ], + [ + 'key' => 'currency_id', + 'required' => false, + ], ]; return $data; diff --git a/app/Filters/BankTransactionFilters.php b/app/Filters/BankTransactionFilters.php index 8bcfc55f1557..a993cb2aa7a5 100644 --- a/app/Filters/BankTransactionFilters.php +++ b/app/Filters/BankTransactionFilters.php @@ -77,28 +77,45 @@ class BankTransactionFilters extends QueryFilters $status_parameters = explode(',', $value); + $status_array = []; + + $debit_or_withdrawal_array = []; + if (in_array('all', $status_parameters)) { return $this->builder; } if (in_array('unmatched', $status_parameters)) { - $this->builder->where('status_id', BankTransaction::STATUS_UNMATCHED); + $status_array[] = BankTransaction::STATUS_UNMATCHED; + // $this->builder->orWhere('status_id', BankTransaction::STATUS_UNMATCHED); } if (in_array('matched', $status_parameters)) { - $this->builder->where('status_id', BankTransaction::STATUS_MATCHED); + $status_array[] = BankTransaction::STATUS_MATCHED; + // $this->builder->where('status_id', BankTransaction::STATUS_MATCHED); } if (in_array('converted', $status_parameters)) { - $this->builder->where('status_id', BankTransaction::STATUS_CONVERTED); + $status_array[] = BankTransaction::STATUS_CONVERTED; + // $this->builder->where('status_id', BankTransaction::STATUS_CONVERTED); } if (in_array('deposits', $status_parameters)) { - $this->builder->where('base_type', 'CREDIT'); + $debit_or_withdrawal_array[] = 'CREDIT'; + // $this->builder->where('base_type', 'CREDIT'); } if (in_array('withdrawals', $status_parameters)) { - $this->builder->where('base_type', 'DEBIT'); + $debit_or_withdrawal_array[] = 'DEBIT'; + // $this->builder->where('base_type', 'DEBIT'); + } + + if(count($status_array) >=1) { + $this->builder->whereIn('status_id', $status_array); + } + + if(count($debit_or_withdrawal_array) >=1) { + $this->builder->orWhereIn('base_type', $debit_or_withdrawal_array); } return $this->builder; diff --git a/app/Filters/ClientFilters.php b/app/Filters/ClientFilters.php index 422842a46c4b..cd19453991f1 100644 --- a/app/Filters/ClientFilters.php +++ b/app/Filters/ClientFilters.php @@ -238,7 +238,6 @@ class ClientFilters extends QueryFilters */ public function entityFilter() { - //return $this->builder->whereCompanyId(auth()->user()->company()->id); return $this->builder->company(); } } diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 410da19872e6..8d412853539f 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -47,20 +47,27 @@ class InvoiceFilters extends QueryFilters $status_parameters = explode(',', $value); + $invoice_filters = []; + if (in_array('all', $status_parameters)) { return $this->builder; } if (in_array('paid', $status_parameters)) { - $this->builder->where('status_id', Invoice::STATUS_PAID); + $invoice_filters[] = Invoice::STATUS_PAID; } if (in_array('unpaid', $status_parameters)) { - $this->builder->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]); + $invoice_filters[] = Invoice::STATUS_SENT; + $invoice_filters[] = Invoice::STATUS_PARTIAL; } + if(count($invoice_filters) >0){ + $this->builder->whereIn('status_id', $invoice_filters); + } + if (in_array('overdue', $status_parameters)) { - $this->builder->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + $this->builder->orWhereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('due_date', '<', Carbon::now()) ->orWhere('partial_due_date', '<', Carbon::now()); } diff --git a/app/Filters/PurchaseOrderFilters.php b/app/Filters/PurchaseOrderFilters.php index 2d9f957d75f6..77fd4ede62c0 100644 --- a/app/Filters/PurchaseOrderFilters.php +++ b/app/Filters/PurchaseOrderFilters.php @@ -42,20 +42,26 @@ class PurchaseOrderFilters extends QueryFilters return $this->builder; } + $po_status = []; + if (in_array('draft', $status_parameters)) { - $this->builder->where('status_id', PurchaseOrder::STATUS_DRAFT); + $po_status[] = PurchaseOrder::STATUS_DRAFT; } if (in_array('sent', $status_parameters)) { - $this->builder->where('status_id', PurchaseOrder::STATUS_SENT); + $po_status[] = PurchaseOrder::STATUS_SENT; } if (in_array('accepted', $status_parameters)) { - $this->builder->where('status_id', PurchaseOrder::STATUS_ACCEPTED); + $po_status[] = PurchaseOrder::STATUS_ACCEPTED; } if (in_array('cancelled', $status_parameters)) { - $this->builder->where('status_id', PurchaseOrder::STATUS_CANCELLED); + $po_status[] = PurchaseOrder::STATUS_CANCELLED; + } + + if(count($status_parameters) >=1) { + $this->builder->whereIn('status_id', $status_parameters); } return $this->builder; diff --git a/app/Filters/QueryFilters.php b/app/Filters/QueryFilters.php index 895b75b4e6cf..7991172149b9 100644 --- a/app/Filters/QueryFilters.php +++ b/app/Filters/QueryFilters.php @@ -15,6 +15,7 @@ namespace App\Filters; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; /** * Class QueryFilters. @@ -173,22 +174,30 @@ abstract class QueryFilters } } - public function created_at($value) + public function created_at($value = '') { - $created_at = $value ? (int) $value : 0; + + if($value == '') + return $this->builder; - $created_at = date('Y-m-d H:i:s', $value); + try{ - if(is_string($created_at)){ + if(is_numeric($value)){ + $created_at = Carbon::createFromTimestamp((int)$value); + } + else{ + $created_at = Carbon::parse($value); + } - $created_at = strtotime(str_replace("/","-",$created_at)); - - if(!$created_at) - return $this->builder; + return $this->builder->where('created_at', '>=', $created_at); } + catch(\Exception $e) { - return $this->builder->where('created_at', '>=', $created_at); + return $this->builder; + + } + } public function is_deleted($value) diff --git a/app/Filters/QuoteFilters.php b/app/Filters/QuoteFilters.php index 5446c0532da7..116e3ca537cb 100644 --- a/app/Filters/QuoteFilters.php +++ b/app/Filters/QuoteFilters.php @@ -66,26 +66,32 @@ class QuoteFilters extends QueryFilters return $this->builder; } + $quote_filters = []; + if (in_array('draft', $status_parameters)) { - $this->builder->where('status_id', Quote::STATUS_DRAFT); + $quote_filters[] = Quote::STATUS_DRAFT; } if (in_array('sent', $status_parameters)) { - $this->builder->where('status_id', Quote::STATUS_SENT); + $quote_filters[] = Quote::STATUS_SENT; } if (in_array('approved', $status_parameters)) { - $this->builder->where('status_id', Quote::STATUS_APPROVED); + $quote_filters[] = Quote::STATUS_APPROVED; + } + + if(count($quote_filters) >=1){ + $this->builder->whereIn('status_id', $quote_filters); } if (in_array('expired', $status_parameters)) { - $this->builder->where('status_id', Quote::STATUS_SENT) - ->where('due_date', '>=', now()->toDateString()); + $this->builder->orWhere('status_id', Quote::STATUS_SENT) + ->where('due_date', '<=', now()->toDateString()); } if (in_array('upcoming', $status_parameters)) { - $this->builder->where('status_id', Quote::STATUS_SENT) - ->where('due_date', '<=', now()->toDateString()) + $this->builder->orWhere('status_id', Quote::STATUS_SENT) + ->where('due_date', '>=', now()->toDateString()) ->orderBy('due_date', 'DESC'); } diff --git a/app/Http/Controllers/Auth/ContactRegisterController.php b/app/Http/Controllers/Auth/ContactRegisterController.php index db7fc1e0c7bb..c7d24fb26eb8 100644 --- a/app/Http/Controllers/Auth/ContactRegisterController.php +++ b/app/Http/Controllers/Auth/ContactRegisterController.php @@ -51,6 +51,7 @@ class ContactRegisterController extends Controller public function register(RegisterRequest $request) { + $request->merge(['company' => $request->company()]); $client = $this->getClient($request->all()); @@ -58,7 +59,7 @@ class ContactRegisterController extends Controller Auth::guard('contact')->loginUsingId($client_contact->id, true); - return redirect()->route('client.dashboard'); + return redirect()->intended(route('client.dashboard')); } private function getClient(array $data) @@ -66,7 +67,15 @@ class ContactRegisterController extends Controller $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(); diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 622b64d20e73..607c7a27ce0c 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -52,7 +52,7 @@ class InvoiceController extends Controller * * @return Factory|View */ - public function show(ShowInvoiceRequest $request, Invoice $invoice) + public function show(ShowInvoiceRequest $request, Invoice $invoice, ?string $hash = null) { set_time_limit(0); @@ -69,6 +69,7 @@ class InvoiceController extends Controller 'invoice' => $invoice, 'invitation' => $invitation ?: $invoice->invitations->first(), 'key' => $invitation ? $invitation->key : false, + 'hash' => $hash, ]; if ($request->query('mode') === 'fullscreen') { diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 6e3e72bb3a95..9267853d0af6 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -148,8 +148,17 @@ class PaymentController extends Controller $payment = $payment->service()->applyCredits($payment_hash)->save(); + $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id'))); + event('eloquent.created: App\Models\Payment', $payment); + if($invoices->sum('balance') > 0){ + + $invoice = $invoices->first(); + + return redirect()->route('client.invoice.show', ['invoice' => $invoice->hashed_id, 'hash' => $request->input('hash')]); + } + if (property_exists($payment_hash->data, 'billing_context')) { $billing_subscription = \App\Models\Subscription::find($payment_hash->data->billing_context->subscription_id); diff --git a/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php b/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php index b5b4593a52fa..aa829ef457e0 100644 --- a/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php +++ b/app/Http/Controllers/ClientPortal/SubscriptionPlanSwitchController.php @@ -33,7 +33,9 @@ class SubscriptionPlanSwitchController extends Controller { $amount = $recurring_invoice->subscription ->service() - ->calculateUpgradePrice($recurring_invoice, $target); + ->calculateUpgradePriceV2($recurring_invoice, $target); + + nlog("payment amount = {$amount}"); /** * Null value here is a proxy for * denying the user a change plan option @@ -42,6 +44,9 @@ class SubscriptionPlanSwitchController extends Controller render('subscriptions.denied'); } + + $amount = max(0,$amount); + return render('subscriptions.switch', [ 'subscription' => $recurring_invoice->subscription, 'recurring_invoice' => $recurring_invoice, diff --git a/app/Http/Controllers/ClientPortal/SubscriptionPurchaseController.php b/app/Http/Controllers/ClientPortal/SubscriptionPurchaseController.php index fb569df26c3d..04354e228220 100644 --- a/app/Http/Controllers/ClientPortal/SubscriptionPurchaseController.php +++ b/app/Http/Controllers/ClientPortal/SubscriptionPurchaseController.php @@ -53,6 +53,16 @@ class SubscriptionPurchaseController extends Controller $this->setLocale($request->query('locale')); } + if(!auth()->guard('contact')->check() && $subscription->registration_required && $subscription->company->client_can_register) { + + session()->put('url.intended', route('client.subscription.upgrade',['subscription' => $subscription->hashed_id])); + + return redirect()->route('client.register', ['company_key' => $subscription->company->company_key]); + } + elseif(!auth()->guard('contact')->check() && $subscription->registration_required && ! $subscription->company->client_can_register) { + return render('generic.subscription_blocked', ['account' => $subscription->company->account, 'company' => $subscription->company]); + } + return view('billing-portal.purchasev2', [ 'subscription' => $subscription, 'hash' => Str::uuid()->toString(), diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 114b61016eae..23e5208ee075 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -521,7 +521,7 @@ class CompanyController extends BaseController $nmo->company = $other_company; $nmo->settings = $other_company->settings; $nmo->to_user = auth()->user(); - NinjaMailerJob::dispatch($nmo, true); + (new NinjaMailerJob($nmo, true))->handle(); $company->delete(); diff --git a/app/Http/Controllers/ExpenseCategoryController.php b/app/Http/Controllers/ExpenseCategoryController.php index 83d6be93e412..f8780baa27be 100644 --- a/app/Http/Controllers/ExpenseCategoryController.php +++ b/app/Http/Controllers/ExpenseCategoryController.php @@ -135,11 +135,45 @@ class ExpenseCategoryController extends BaseController return $this->itemResponse($expense_category); } + /** * Store a newly created resource in storage. * - * @param StoreExpenseCategoryRequest $request + * @param StoreInvoiceRequest $request The request + * * @return Response + * + * + * @OA\Post( + * path="/api/v1/expense_categories", + * operationId="storeExpenseCategory", + * tags={"expense_categories"}, + * summary="Adds a expense category", + * description="Adds an expense category to the system", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="Returns the saved invoice object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/ExpenseCategory"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) */ public function store(StoreExpenseCategoryRequest $request) { diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 47cbcd68aeec..452c5895374d 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -195,10 +195,10 @@ class PurchaseOrderController extends BaseController ->fillDefaults() ->triggeredActions($request) ->save(); - + event(new PurchaseOrderWasCreated($purchase_order, $purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); - return $this->itemResponse($purchase_order); + return $this->itemResponse($purchase_order->fresh()); } /** * Display the specified resource. diff --git a/app/Http/Livewire/BillingPortalPurchase.php b/app/Http/Livewire/BillingPortalPurchase.php index 3433f07a58df..404194f58531 100644 --- a/app/Http/Livewire/BillingPortalPurchase.php +++ b/app/Http/Livewire/BillingPortalPurchase.php @@ -174,6 +174,8 @@ class BillingPortalPurchase extends Component */ public $company; + public $db; + /** * Campaign reference. * @@ -183,7 +185,11 @@ class BillingPortalPurchase extends Component public function mount() { - MultiDB::setDb($this->company->db); + MultiDB::setDb($this->db); + + $this->subscription = Subscription::with('company')->find($this->subscription); + + $this->company = $this->subscription->company; $this->quantity = 1; @@ -225,10 +231,10 @@ class BillingPortalPurchase extends Component $this->steps['existing_user'] = false; - $contact = $this->createBlankClient(); + $this->contact = $this->createBlankClient(); - if ($contact && $contact instanceof ClientContact) { - $this->getPaymentMethods($contact); + if ($this->contact && $this->contact instanceof ClientContact) { + $this->getPaymentMethods($this->contact); } } @@ -265,9 +271,6 @@ class BillingPortalPurchase extends Component } } -// nlog($this->subscription->group_settings->settings); -// nlog($this->subscription->group_settings->settings->currency_id); - if(array_key_exists('currency_id', $this->request_data)) { $currency = Cache::get('currencies')->filter(function ($item){ diff --git a/app/Http/Livewire/BillingPortalPurchasev2.php b/app/Http/Livewire/BillingPortalPurchasev2.php index dd2bf843e024..8ff7b6114434 100644 --- a/app/Http/Livewire/BillingPortalPurchasev2.php +++ b/app/Http/Livewire/BillingPortalPurchasev2.php @@ -15,6 +15,7 @@ use App\DataMapper\ClientSettings; use App\Factory\ClientFactory; use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerObject; +use App\Jobs\Subscription\CleanStaleInvoiceOrder; use App\Libraries\MultiDB; use App\Mail\ContactPasswordlessLogin; use App\Mail\Subscription\OtpCode; @@ -120,7 +121,7 @@ class BillingPortalPurchasev2 extends Component * * @var array */ - public $request_data; + public $request_data = []; /** * Instance of company. @@ -129,6 +130,14 @@ class BillingPortalPurchasev2 extends Component */ public $company; + + /** + * Instance of company. + * + * @var string + */ + public string $db; + /** * Campaign reference. * @@ -151,10 +160,23 @@ class BillingPortalPurchasev2 extends Component public $valid_coupon = false; public $payable_invoices = []; public $payment_confirmed = false; - + public $is_eligible = true; + public $not_eligible_message = ''; + public function mount() { - MultiDB::setDb($this->company->db); + MultiDB::setDb($this->db); + + $this->subscription = Subscription::with('company')->find($this->subscription); + + $this->company = $this->subscription->company; + + if(auth()->guard('contact')->check()){ + $this->email = auth()->guard('contact')->user()->email; + $this->contact = auth()->guard('contact')->user(); + $this->authenticated = true; + $this->payment_started = true; + } $this->discount = 0; $this->sub_total = 0; @@ -177,7 +199,7 @@ class BillingPortalPurchasev2 extends Component $this->coupon = request()->query('coupon'); $this->handleCoupon(); } - elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){ + elseif(isset($this->subscription->promo_code) && strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){ $this->price = $this->subscription->promo_price; } @@ -224,6 +246,8 @@ class BillingPortalPurchasev2 extends Component public function resetEmail() { + $this->resetErrorBag('login'); + $this->resetValidation('login'); $this->email = null; } @@ -449,8 +473,6 @@ class BillingPortalPurchasev2 extends Component $this->buildBundle(); -nlog($this->bundle); - return $this; } @@ -489,9 +511,20 @@ nlog($this->bundle); * * @return void */ - public function handleBeforePaymentEvents() :void + public function handleBeforePaymentEvents() :self { + $eligibility_check = $this->subscription->service()->isEligible($this->contact); + + if(is_array($eligibility_check) && $eligibility_check['message'] != 'Success'){ + + $this->is_eligible = false; + $this->not_eligible_message = $eligibility_check['message']; + + return $this; + + } + $data = [ 'client_id' => $this->contact->client->id, 'date' => now()->format('Y-m-d'), @@ -501,19 +534,9 @@ nlog($this->bundle); ]], 'user_input_promo_code' => $this->coupon, 'coupon' => empty($this->subscription->promo_code) ? '' : $this->coupon, - // 'quantity' => $this->quantity, + ]; - $is_eligible = $this->subscription->service()->isEligible($this->contact); - - // if (is_array($is_eligible) && $is_eligible['message'] != 'Success') { - // $this->steps['not_eligible'] = true; - // $this->steps['not_eligible_message'] = $is_eligible['message']; - // $this->steps['show_loading_bar'] = false; - - // return; - // } - $this->invoice = $this->subscription ->service() ->createInvoiceV2($this->bundle, $this->contact->client_id, $this->valid_coupon) @@ -534,6 +557,9 @@ nlog($this->bundle); ], now()->addMinutes(60)); $this->emit('beforePaymentEventsCompleted'); + + return $this; + } public function handleTrial() diff --git a/app/Http/Livewire/CreditsTable.php b/app/Http/Livewire/CreditsTable.php index b347e6486c6e..afdc66545d73 100644 --- a/app/Http/Livewire/CreditsTable.php +++ b/app/Http/Livewire/CreditsTable.php @@ -13,6 +13,7 @@ namespace App\Http\Livewire; use App\Libraries\MultiDB; +use App\Models\Company; use App\Models\Credit; use App\Utils\Traits\WithSorting; use Livewire\Component; @@ -23,26 +24,31 @@ class CreditsTable extends Component use WithPagination; use WithSorting; - public $per_page = 10; + public int $per_page = 10; - public $company; + public Company $company; + + public string $db; + + public int $company_id; public function mount() { - MultiDB::setDb($this->company->db); + MultiDB::setDb($this->db); + + $this->company = Company::find($this->company_id); } public function render() { $query = Credit::query() - ->where('client_id', auth()->guard('contact')->user()->client_id) ->where('company_id', $this->company->id) + ->where('client_id', auth()->guard('contact')->user()->client_id) ->where('status_id', '<>', Credit::STATUS_DRAFT) ->where('is_deleted', 0) ->where(function ($query) { $query->whereDate('due_date', '>=', now()) ->orWhereNull('due_date'); - //->orWhere('due_date', '=', ''); }) ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->withTrashed() diff --git a/app/Http/Livewire/DocumentsTable.php b/app/Http/Livewire/DocumentsTable.php index e5748d80e3a8..60b3b761dde9 100644 --- a/app/Http/Livewire/DocumentsTable.php +++ b/app/Http/Livewire/DocumentsTable.php @@ -14,6 +14,7 @@ namespace App\Http\Livewire; use App\Libraries\MultiDB; use App\Models\Client; +use App\Models\Company; use App\Models\Credit; use App\Models\Document; use App\Models\Expense; @@ -31,21 +32,27 @@ class DocumentsTable extends Component { use WithPagination, WithSorting; - public $client; + public Company $company; - public $per_page = 10; + public Client $client; - public $company; + public int $client_id; + + public int $per_page = 10; public string $tab = 'documents'; + public string $db; + protected $query; - public function mount($client) + public function mount() { - MultiDB::setDb($this->company->db); + MultiDB::setDb($this->db); - $this->client = $client; + $this->client = Client::with('company')->find($this->client_id); + + $this->company = $this->client->company; $this->query = $this->documents(); } diff --git a/app/Http/Livewire/InvoicesTable.php b/app/Http/Livewire/InvoicesTable.php index f35715e17867..3bfaa9390ad9 100644 --- a/app/Http/Livewire/InvoicesTable.php +++ b/app/Http/Livewire/InvoicesTable.php @@ -13,6 +13,7 @@ namespace App\Http\Livewire; use App\Libraries\MultiDB; +use App\Models\Company; use App\Models\Invoice; use App\Utils\Traits\WithSorting; use Carbon\Carbon; @@ -23,15 +24,21 @@ class InvoicesTable extends Component { use WithPagination, WithSorting; - public $per_page = 10; + public int $per_page = 10; - public $status = []; + public array $status = []; - public $company; + public Company $company; + + public int $company_id; + + public string $db; public function mount() { - MultiDB::setDb($this->company->db); + MultiDB::setDb($this->db); + + $this->company = Company::find($this->company_id); $this->sort_asc = false; diff --git a/app/Http/Livewire/PayNowDropdown.php b/app/Http/Livewire/PayNowDropdown.php index d0b1e1190b33..077e78b05d4f 100644 --- a/app/Http/Livewire/PayNowDropdown.php +++ b/app/Http/Livewire/PayNowDropdown.php @@ -23,13 +23,11 @@ class PayNowDropdown extends Component public $company; - public function mount(int $total) + public function mount() { MultiDB::setDb($this->company->db); - $this->total = $total; - - $this->methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods($total); + $this->methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods($this->total); } public function render() diff --git a/app/Http/Livewire/PaymentMethodsTable.php b/app/Http/Livewire/PaymentMethodsTable.php index 881d30afda2f..d0f85db911d3 100644 --- a/app/Http/Livewire/PaymentMethodsTable.php +++ b/app/Http/Livewire/PaymentMethodsTable.php @@ -3,7 +3,9 @@ namespace App\Http\Livewire; use App\Libraries\MultiDB; +use App\Models\Client; use App\Models\ClientGatewayToken; +use App\Models\Company; use App\Utils\Traits\WithSorting; use Livewire\Component; use Livewire\WithPagination; @@ -13,17 +15,23 @@ class PaymentMethodsTable extends Component use WithPagination; use WithSorting; - public $per_page = 10; + public int $per_page = 10; - public $client; + public Client $client; - public $company; + public Company $company; - public function mount($client) + public int $client_id; + + public string $db; + + public function mount() { - MultiDB::setDb($this->company->db); + MultiDB::setDb($this->db); - $this->client = $client; + $this->client = Client::with('company')->find($this->client_id); + + $this->company = $this->client->company; } public function render() diff --git a/app/Http/Livewire/PaymentsTable.php b/app/Http/Livewire/PaymentsTable.php index 208f3cc549bd..1ad99fa2bbcf 100644 --- a/app/Http/Livewire/PaymentsTable.php +++ b/app/Http/Livewire/PaymentsTable.php @@ -13,6 +13,7 @@ namespace App\Http\Livewire; use App\Libraries\MultiDB; +use App\Models\Company; use App\Models\Payment; use App\Utils\Traits\WithSorting; use Livewire\Component; @@ -23,17 +24,19 @@ class PaymentsTable extends Component use WithSorting; use WithPagination; - public $per_page = 10; + public int $per_page = 10; - public $user; + public Company $company; - public $company; + public int $company_id; + + public string $db; public function mount() { - MultiDB::setDb($this->company->db); + MultiDB::setDb($this->db); - $this->user = auth()->user(); + $this->company = Company::find($this->company_id); } public function render() diff --git a/app/Http/Livewire/QuotesTable.php b/app/Http/Livewire/QuotesTable.php index 93f416703968..da6785e825a6 100644 --- a/app/Http/Livewire/QuotesTable.php +++ b/app/Http/Livewire/QuotesTable.php @@ -13,6 +13,7 @@ namespace App\Http\Livewire; use App\Libraries\MultiDB; +use App\Models\Company; use App\Models\Quote; use App\Utils\Traits\WithSorting; use Livewire\Component; @@ -22,15 +23,27 @@ class QuotesTable extends Component { use WithPagination; - public $per_page = 10; + public int $per_page = 10; - public $status = []; + public array $status = []; - public $company; + public Company $company; - public $sort = 'status_id'; // Default sortBy. Feel free to change or pull from client/company settings. + public string $sort = 'status_id'; + + public bool $sort_asc = true; + + public int $company_id; + + public string $db; + + public function mount() + { + MultiDB::setDb($this->db); + + $this->company = Company::find($this->company_id); + } - public $sort_asc = true; public function sortBy($field) { @@ -41,16 +54,11 @@ class QuotesTable extends Component $this->sort = $field; } - public function mount() - { - MultiDB::setDb($this->company->db); - } - public function render() { $query = Quote::query() - ->with('client.gateway_tokens', 'company', 'client.contacts') + ->with('client.contacts', 'company') ->orderBy($this->sort, $this->sort_asc ? 'asc' : 'desc'); if (count($this->status) > 0) { diff --git a/app/Http/Livewire/SubscriptionPlanSwitch.php b/app/Http/Livewire/SubscriptionPlanSwitch.php index aac881662462..47e4ce5a0935 100644 --- a/app/Http/Livewire/SubscriptionPlanSwitch.php +++ b/app/Http/Livewire/SubscriptionPlanSwitch.php @@ -142,7 +142,7 @@ class SubscriptionPlanSwitch extends Component { $this->hide_button = true; - $response = $this->target->service()->createChangePlanCredit([ + $response = $this->target->service()->createChangePlanCreditV2([ 'recurring_invoice' => $this->recurring_invoice, 'subscription' => $this->subscription, 'target' => $this->target, diff --git a/app/Http/Middleware/PasswordProtection.php b/app/Http/Middleware/PasswordProtection.php index 456bb3cf9d77..a4be814fcb47 100644 --- a/app/Http/Middleware/PasswordProtection.php +++ b/app/Http/Middleware/PasswordProtection.php @@ -18,6 +18,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; +use Laravel\Socialite\Facades\Socialite; use stdClass; class PasswordProtection @@ -111,7 +112,18 @@ class PasswordProtection return $next($request); } } + elseif(auth()->user()->oauth_provider_id == 'apple') + { + + $user = Socialite::driver('apple')->userFromToken($request->header('X-API-OAUTH-PASSWORD')); + if($user && ($user->email == auth()->user()->email)){ + + Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); + return $next($request); + } + + } return response()->json($error, 412); diff --git a/app/Http/Requests/ClientPortal/RegisterRequest.php b/app/Http/Requests/ClientPortal/RegisterRequest.php index 26a0aa825315..b795ee609f6f 100644 --- a/app/Http/Requests/ClientPortal/RegisterRequest.php +++ b/app/Http/Requests/ClientPortal/RegisterRequest.php @@ -47,7 +47,7 @@ class RegisterRequest extends FormRequest foreach ($rules as $field => $properties) { if ($field === 'email') { - $rules[$field] = array_merge($rules[$field], ['email:rfc,dns', 'max:255', Rule::unique('client_contacts')->where('company_id', $this->company()->id)]); + $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') { diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index 79934b23820c..ab9181a31ce0 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -110,7 +110,8 @@ class UpdateCompanyRequest extends Request } } - $settings['email_style_custom'] = str_replace(['{{','}}'], ['',''], $settings['email_style_custom']); + if(isset($settings['email_style_custom'])) + $settings['email_style_custom'] = str_replace(['{{','}}'], ['',''], $settings['email_style_custom']); if (! $account->isFreeHostedClient()) { return $settings; diff --git a/app/Http/Requests/Expense/StoreExpenseRequest.php b/app/Http/Requests/Expense/StoreExpenseRequest.php index a36369f01e90..69d297127ce4 100644 --- a/app/Http/Requests/Expense/StoreExpenseRequest.php +++ b/app/Http/Requests/Expense/StoreExpenseRequest.php @@ -41,7 +41,7 @@ class StoreExpenseRequest extends Request $rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id); } - if (! empty($this->client_id)) { + if ($this->client_id) { $rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id; } diff --git a/app/Http/Requests/Expense/UpdateExpenseRequest.php b/app/Http/Requests/Expense/UpdateExpenseRequest.php index 348cad69ddba..05dae8421288 100644 --- a/app/Http/Requests/Expense/UpdateExpenseRequest.php +++ b/app/Http/Requests/Expense/UpdateExpenseRequest.php @@ -41,6 +41,10 @@ class UpdateExpenseRequest extends Request $rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->expense->id); } + if ($this->client_id) { + $rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id; + } + $rules['category_id'] = 'bail|sometimes|nullable|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0'; return $this->globalRules($rules); diff --git a/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php index ef256795c05f..7bc220a8932d 100644 --- a/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php +++ b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php @@ -43,6 +43,7 @@ class StoreRecurringExpenseRequest extends Request $rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id; } + $rules['category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0'; $rules['frequency_id'] = 'required|integer|digits_between:1,12'; $rules['tax_amount1'] = 'numeric'; $rules['tax_amount2'] = 'numeric'; @@ -61,10 +62,6 @@ class StoreRecurringExpenseRequest extends Request $input['next_send_date_client'] = $input['next_send_date']; } - if (array_key_exists('category_id', $input) && is_string($input['category_id'])) { - $input['category_id'] = $this->decodePrimaryKey($input['category_id']); - } - if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { $input['currency_id'] = (string) auth()->user()->company()->settings->currency_id; } diff --git a/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php index 0909c775c880..37540310c169 100644 --- a/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php +++ b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php @@ -46,6 +46,7 @@ class UpdateRecurringExpenseRequest extends Request $rules['tax_amount1'] = 'numeric'; $rules['tax_amount2'] = 'numeric'; $rules['tax_amount3'] = 'numeric'; + $rules['category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0'; return $this->globalRules($rules); } @@ -70,10 +71,6 @@ class UpdateRecurringExpenseRequest extends Request $input['next_send_date_client'] = $input['next_send_date']; } - if (array_key_exists('category_id', $input) && is_string($input['category_id'])) { - $input['category_id'] = $this->decodePrimaryKey($input['category_id']); - } - if (array_key_exists('documents', $input)) { unset($input['documents']); } diff --git a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php new file mode 100644 index 000000000000..6517ea331673 --- /dev/null +++ b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php @@ -0,0 +1,80 @@ +withTrashed() + ->where('is_proforma', 1) + ->where('created_at', '<', now()->subHour()) + ->cursor() + ->each(function ($invoice) use ($repo) { + $invoice->is_proforma = false; + $repo->delete($invoice); + }); + + return; + } + + + foreach (MultiDB::$dbs as $db) + { + + MultiDB::setDB($db); + + Invoice::query() + ->withTrashed() + ->where('is_proforma', 1) + ->where('created_at', '<', now()->subHour()) + ->cursor() + ->each(function ($invoice) use ($repo) { + $invoice->is_proforma = false; + $repo->delete($invoice); + }); + + } + + } + + public function failed($exception = null) + { + } +} diff --git a/app/Listeners/PurchaseOrder/PurchaseOrderAcceptedNotification.php b/app/Listeners/PurchaseOrder/PurchaseOrderAcceptedListener.php similarity index 96% rename from app/Listeners/PurchaseOrder/PurchaseOrderAcceptedNotification.php rename to app/Listeners/PurchaseOrder/PurchaseOrderAcceptedListener.php index d65681cc5ec4..ff64295ee7b5 100644 --- a/app/Listeners/PurchaseOrder/PurchaseOrderAcceptedNotification.php +++ b/app/Listeners/PurchaseOrder/PurchaseOrderAcceptedListener.php @@ -21,12 +21,10 @@ use App\Notifications\Admin\EntitySentNotification; use App\Utils\Traits\Notifications\UserNotifies; use Illuminate\Contracts\Queue\ShouldQueue; -class PurchaseOrderAcceptedNotification implements ShouldQueue +class PurchaseOrderAcceptedListener implements ShouldQueue { use UserNotifies; - public $delay = 5; - public function __construct() { } diff --git a/app/Listeners/PurchaseOrder/PurchaseOrderCreatedListener.php b/app/Listeners/PurchaseOrder/PurchaseOrderCreatedListener.php new file mode 100644 index 000000000000..088aaf81c380 --- /dev/null +++ b/app/Listeners/PurchaseOrder/PurchaseOrderCreatedListener.php @@ -0,0 +1,81 @@ +company->db); + + $first_notification_sent = true; + + $purchase_order = $event->purchase_order; + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new EntityCreatedObject($purchase_order, 'purchase_order'))->build()); + $nmo->company = $purchase_order->company; + $nmo->settings = $purchase_order->company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($event->company->company_users as $company_user) { + + /* The User */ + $user = $company_user->user; + + if (! $user) { + continue; + } + + /* This is only here to handle the alternate message channels - ie Slack */ + // $notification = new EntitySentNotification($event->invitation, 'purchase_order'); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($purchase_order->invitations()->first(), $company_user, 'purchase_order', ['all_notifications', 'purchase_order_created', 'purchase_order_created_all']); + /* If one of the methods is email then we fire the EntitySentMailer */ + + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + + /* This prevents more than one notification being sent */ + $first_notification_sent = false; + } + } + } +} diff --git a/app/Listeners/PurchaseOrder/PurchaseOrderEmailedNotification.php b/app/Listeners/PurchaseOrder/PurchaseOrderEmailedNotification.php new file mode 100644 index 000000000000..36cc203526cf --- /dev/null +++ b/app/Listeners/PurchaseOrder/PurchaseOrderEmailedNotification.php @@ -0,0 +1,85 @@ +company->db); + + $first_notification_sent = true; + + $purchase_order = $event->invitation->purchase_order->fresh(); + $purchase_order->last_sent_date = now(); + $purchase_order->saveQuietly(); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new EntitySentObject($event->invitation, 'purchase_order', 'purchase_order'))->build()); + $nmo->company = $purchase_order->company; + $nmo->settings = $purchase_order->company->settings; + + /* We loop through each user and determine whether they need to be notified */ + foreach ($event->invitation->company->company_users as $company_user) { + + /* The User */ + $user = $company_user->user; + + /* This is only here to handle the alternate message channels - ie Slack */ + // $notification = new EntitySentNotification($event->invitation, 'purchase_order'); + + /* Returns an array of notification methods */ + $methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'purchase_order', ['all_notifications', 'purchase_order_sent', 'purchase_order_sent_all']); + + /* If one of the methods is email then we fire the EntitySentMailer */ + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + + /* This prevents more than one notification being sent */ + $first_notification_sent = false; + } + + /* Override the methods in the Notification Class */ + // $notification->method = $methods; + + // Notify on the alternate channels + // $user->notify($notification); + } + } +} diff --git a/app/Mail/Admin/AutoBillingFailureObject.php b/app/Mail/Admin/AutoBillingFailureObject.php index abbafc45768b..0c69cc073f98 100644 --- a/app/Mail/Admin/AutoBillingFailureObject.php +++ b/app/Mail/Admin/AutoBillingFailureObject.php @@ -64,7 +64,7 @@ class AutoBillingFailureObject /* Set customized translations _NOW_ */ $t->replace(Ninja::transformTranslations($this->company->settings)); - $this->$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get(); + $this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get(); $mail_obj = new stdClass; $mail_obj->amount = $this->getAmount(); diff --git a/app/Mail/Admin/EntityCreatedObject.php b/app/Mail/Admin/EntityCreatedObject.php index 38555b06d1ec..2cec34303b6a 100644 --- a/app/Mail/Admin/EntityCreatedObject.php +++ b/app/Mail/Admin/EntityCreatedObject.php @@ -13,6 +13,7 @@ namespace App\Mail\Admin; use App\Utils\Ninja; use App\Utils\Number; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Facades\App; use stdClass; @@ -38,7 +39,11 @@ class EntityCreatedObject $this->entity = $entity; } - public function build() + /** + * @return stdClass + * @throws BindingResolutionException + */ + public function build() :stdClass { App::forgetInstance('translator'); /* Init a new copy of the translator*/ @@ -47,26 +52,64 @@ class EntityCreatedObject App::setLocale($this->entity->company->getLocale()); /* Set customized translations _NOW_ */ $t->replace(Ninja::transformTranslations($this->entity->company->settings)); - - $this->entity->load('client.country', 'client.company'); - $this->client = $this->entity->client; + $this->setTemplate(); $this->company = $this->entity->company; - $this->setTemplate(); + if($this->entity_type == 'purchase_order') + { - $mail_obj = new stdClass; - $mail_obj->amount = $this->getAmount(); - $mail_obj->subject = $this->getSubject(); - $mail_obj->data = $this->getData(); - $mail_obj->markdown = 'email.admin.generic'; - $mail_obj->tag = $this->company->company_key; + $this->entity->load('vendor.company'); + $mail_obj = new stdClass; + $mail_obj->amount = Number::formatMoney($this->entity->amount, $this->entity->vendor); + + $mail_obj->subject = ctrans($this->template_subject, + [ + 'vendor' => $this->entity->vendor->present()->name(), + 'purchase_order' => $this->entity->number, + ] + ); + + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + $mail_obj->data = [ + 'title' => $mail_obj->subject, + 'message' => ctrans($this->template_body, + [ + 'amount' => $mail_obj->amount, + 'vendor' => $this->entity->vendor->present()->name(), + 'purchase_order' => $this->entity->number, + ] + ), + 'url' => $this->entity->invitations()->first()->getAdminLink(), + 'button' => ctrans("texts.view_{$this->entity_type}"), + 'signature' => $this->company->settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $this->company->settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + ]; + + + } + else { + + $this->entity->load('client.country', 'client.company'); + $this->client = $this->entity->client; + + $mail_obj = new stdClass; + $mail_obj->amount = $this->getAmount(); + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->entity->company->company_key; + + } + return $mail_obj; } private function setTemplate() { - // nlog($this->template); switch ($this->entity_type) { case 'invoice': @@ -81,7 +124,10 @@ class EntityCreatedObject $this->template_subject = 'texts.notification_credit_created_subject'; $this->template_body = 'texts.notification_credit_created_body'; break; - + case 'purchase_order': + $this->template_subject = 'texts.notification_purchase_order_created_subject'; + $this->template_body = 'texts.notification_purchase_order_created_body'; + break; default: $this->template_subject = 'texts.notification_invoice_created_subject'; $this->template_body = 'texts.notification_invoice_created_body'; @@ -89,7 +135,7 @@ class EntityCreatedObject } } - private function getAmount() + private function getAmount() { return Number::formatMoney($this->entity->amount, $this->entity->client); } diff --git a/app/Mail/Admin/EntitySentObject.php b/app/Mail/Admin/EntitySentObject.php index b960270ad717..fe636efc571c 100644 --- a/app/Mail/Admin/EntitySentObject.php +++ b/app/Mail/Admin/EntitySentObject.php @@ -58,13 +58,48 @@ class EntitySentObject $this->setTemplate(); - $mail_obj = new stdClass; - $mail_obj->amount = $this->getAmount(); - $mail_obj->subject = $this->getSubject(); - $mail_obj->data = $this->getData(); - $mail_obj->markdown = 'email.admin.generic'; - $mail_obj->tag = $this->company->company_key; + if($this->template == 'purchase_order') + { + $mail_obj = new stdClass; + $mail_obj->amount = Number::formatMoney($this->entity->amount, $this->entity->vendor); + $mail_obj->subject = ctrans($this->template_subject, + [ + 'vendor' => $this->contact->vendor->present()->name(), + 'purchase_order' => $this->entity->number, + ] + ); + $mail_obj->data = [ + 'title' => $mail_obj->subject, + 'message' => ctrans($this->template_body, + [ + 'amount' => $mail_obj->amount, + 'vendor' => $this->contact->vendor->present()->name(), + 'purchase_order' => $this->entity->number, + ] + ), + 'url' => $this->invitation->getAdminLink(), + 'button' => ctrans("texts.view_{$this->entity_type}"), + 'signature' => $this->company->settings->email_signature, + 'logo' => $this->company->present()->logo(), + 'settings' => $this->company->settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + ]; + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + } + else { + + $mail_obj = new stdClass; + $mail_obj->amount = $this->getAmount(); + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + } + return $mail_obj; } @@ -101,7 +136,10 @@ class EntitySentObject $this->template_subject = 'texts.notification_credit_sent_subject'; $this->template_body = 'texts.notification_credit_sent'; break; - + case 'purchase_order': + $this->template_subject = 'texts.notification_purchase_order_sent_subject'; + $this->template_body = 'texts.notification_purchase_order_sent'; + break; default: $this->template_subject = 'texts.notification_invoice_sent_subject'; $this->template_body = 'texts.notification_invoice_sent'; diff --git a/app/Models/Company.php b/app/Models/Company.php index 36daf77e02a4..7864220ca00d 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -129,6 +129,7 @@ class Company extends BaseModel 'invoice_task_lock', 'convert_payment_currency', 'convert_expense_currency', + 'notify_vendor_when_paid', ]; protected $hidden = [ @@ -138,6 +139,7 @@ class Company extends BaseModel ]; protected $casts = [ + 'is_proforma' => 'bool', 'country_id' => 'string', 'custom_fields' => 'object', 'settings' => 'object', diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 24fcf324ff5b..abde54bf77c2 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -117,6 +117,11 @@ class Vendor extends BaseModel } + public function timezone() + { + return $this->company->timezone(); + } + public function company() { return $this->belongsTo(Company::class); diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 5c79eb531a0a..e17e3f648518 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -325,15 +325,6 @@ class BaseDriver extends AbstractPaymentDriver $invoice->service()->toggleFeesPaid()->save(); } - $transaction = [ - 'invoice' => $invoice->transaction_event(), - 'payment' => [], - 'client' => $invoice->client->transaction_event(), - 'credit' => [], - 'metadata' => [], - ]; - - // TransactionLog::dispatch(TransactionEvent::INVOICE_FEE_APPLIED, $transaction, $invoice->company->db); }); } diff --git a/app/PaymentDrivers/CheckoutCom/CreditCard.php b/app/PaymentDrivers/CheckoutCom/CreditCard.php index 1ada04574b5e..b6a05d989793 100644 --- a/app/PaymentDrivers/CheckoutCom/CreditCard.php +++ b/app/PaymentDrivers/CheckoutCom/CreditCard.php @@ -124,18 +124,20 @@ class CreditCard implements MethodInterface } } catch (CheckoutApiException $e) { // API error - $request_id = $e->request_id; - $http_status_code = $e->http_status_code; + $request_id = $e->request_id ?: ''; + $http_status_code = $e->http_status_code ?: ''; $error_details = $e->error_details; if(is_array($error_details)) { $error_details = end($e->error_details['error_codes']); } - $human_exception = $error_details ? new \Exception($error_details, 400) : $e; + $human_exception = $error_details ? $error_details : $e->getMessage(); + $human_exception = "{$human_exception} - Request ID: {$request_id}"; + + throw new PaymentFailed($human_exception, $http_status_code); - throw new PaymentFailed($human_exception); } catch (CheckoutArgumentException $e) { // Bad arguments @@ -145,9 +147,9 @@ class CreditCard implements MethodInterface $error_details = end($e->error_details['error_codes']); } - $human_exception = $error_details ? new \Exception($error_details, 400) : $e; + $human_exception = $error_details ? $error_details : $e->getMessage(); - throw new PaymentFailed($human_exception); + throw new PaymentFailed($human_exception, 422); } catch (CheckoutAuthorizationException $e) { // Bad Invalid authorization @@ -157,9 +159,9 @@ class CreditCard implements MethodInterface $error_details = end($e->error_details['error_codes']); } - $human_exception = $error_details ? new \Exception($error_details, 400) : $e; + $human_exception = $error_details ? $error_details : $e->getMessage(); - throw new PaymentFailed($human_exception); + throw new PaymentFailed($human_exception, 401); } } @@ -228,7 +230,7 @@ class CreditCard implements MethodInterface private function completePayment($paymentRequest, PaymentResponseRequest $request) { $paymentRequest->amount = $this->checkout->payment_hash->data->value; - $paymentRequest->reference = $this->checkout->getDescription(); + $paymentRequest->reference = substr($this->checkout->getDescription(),0 , 49); $paymentRequest->customer = $this->checkout->getCustomer(); $paymentRequest->metadata = ['udf1' => 'Invoice Ninja']; $paymentRequest->currency = $this->checkout->client->getCurrencyCode(); diff --git a/app/PaymentDrivers/CheckoutCom/Utilities.php b/app/PaymentDrivers/CheckoutCom/Utilities.php index d9d2ff4b3fbb..7ac9601d5d60 100644 --- a/app/PaymentDrivers/CheckoutCom/Utilities.php +++ b/app/PaymentDrivers/CheckoutCom/Utilities.php @@ -87,6 +87,9 @@ trait Utilities $error_message = ''; + nlog("checkout failure"); + nlog($_payment); + if (is_array($_payment) && array_key_exists('actions', $_payment) && array_key_exists('response_summary', end($_payment['actions']))) { $error_message = end($_payment['actions'])['response_summary']; } elseif (is_array($_payment) && array_key_exists('status', $_payment)) { diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index c6a4f9fdbcc9..dee640c44c44 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -627,18 +627,16 @@ class StripePaymentDriver extends BaseDriver public function processWebhookRequest(PaymentWebhookRequest $request) { - // Allow app to catch up with webhook request. - sleep(2); //payment_intent.succeeded - this will confirm or cancel the payment if ($request->type === 'payment_intent.succeeded') { - PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2, 10))); + PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10))); return response()->json([], 200); } if (in_array($request->type, ['payment_intent.payment_failed', 'charge.failed'])) { - PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2, 10))); + PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10))); return response()->json([], 200); } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index cc3a02d52079..2e7b59ebc4cc 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -157,15 +157,14 @@ use App\Listeners\Credit\CreditRestoredActivity; use App\Listeners\Credit\CreditViewedActivity; use App\Listeners\Document\DeleteCompanyDocuments; use App\Listeners\Invoice\CreateInvoiceActivity; -use App\Listeners\Invoice\CreateInvoiceHtmlBackup; use App\Listeners\Invoice\CreateInvoicePdf; use App\Listeners\Invoice\InvoiceArchivedActivity; use App\Listeners\Invoice\InvoiceCancelledActivity; use App\Listeners\Invoice\InvoiceCreatedNotification; use App\Listeners\Invoice\InvoiceDeletedActivity; use App\Listeners\Invoice\InvoiceEmailActivity; -use App\Listeners\Invoice\InvoiceEmailedNotification; use App\Listeners\Invoice\InvoiceEmailFailedActivity; +use App\Listeners\Invoice\InvoiceEmailedNotification; use App\Listeners\Invoice\InvoiceFailedEmailNotification; use App\Listeners\Invoice\InvoicePaidActivity; use App\Listeners\Invoice\InvoiceReminderEmailActivity; @@ -175,18 +174,21 @@ use App\Listeners\Invoice\InvoiceViewedActivity; use App\Listeners\Invoice\UpdateInvoiceActivity; use App\Listeners\Mail\MailSentListener; use App\Listeners\Misc\InvitationViewedListener; -use App\Listeners\Payment\PaymentEmailedActivity; use App\Listeners\Payment\PaymentEmailFailureActivity; +use App\Listeners\Payment\PaymentEmailedActivity; use App\Listeners\Payment\PaymentNotification; use App\Listeners\Payment\PaymentRestoredActivity; use App\Listeners\PurchaseOrder\CreatePurchaseOrderActivity; use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedActivity; -use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedNotification; +use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedListener; use App\Listeners\PurchaseOrder\PurchaseOrderArchivedActivity; +use App\Listeners\PurchaseOrder\PurchaseOrderCreatedListener; use App\Listeners\PurchaseOrder\PurchaseOrderDeletedActivity; use App\Listeners\PurchaseOrder\PurchaseOrderEmailActivity; +use App\Listeners\PurchaseOrder\PurchaseOrderEmailedNotification; use App\Listeners\PurchaseOrder\PurchaseOrderRestoredActivity; use App\Listeners\PurchaseOrder\PurchaseOrderViewedActivity; +use App\Listeners\PurchaseOrder\PurchaseOrderViewedNotification; use App\Listeners\PurchaseOrder\UpdatePurchaseOrderActivity; use App\Listeners\Quote\QuoteApprovedActivity; use App\Listeners\Quote\QuoteApprovedNotification; @@ -219,8 +221,8 @@ use App\Listeners\User\ArchivedUserActivity; use App\Listeners\User\CreatedUserActivity; use App\Listeners\User\DeletedUserActivity; use App\Listeners\User\RestoredUserActivity; -use App\Listeners\User\UpdatedUserActivity; use App\Listeners\User\UpdateUserLastLogin; +use App\Listeners\User\UpdatedUserActivity; use App\Models\Account; use App\Models\Client; use App\Models\Company; @@ -398,7 +400,6 @@ class EventServiceProvider extends ServiceProvider ], //Invoices InvoiceWasMarkedSent::class => [ - CreateInvoiceHtmlBackup::class, ], InvoiceWasUpdated::class => [ UpdateInvoiceActivity::class, @@ -458,12 +459,14 @@ class EventServiceProvider extends ServiceProvider ], PurchaseOrderWasCreated::class => [ CreatePurchaseOrderActivity::class, + PurchaseOrderCreatedListener::class, ], PurchaseOrderWasDeleted::class => [ PurchaseOrderDeletedActivity::class, ], PurchaseOrderWasEmailed::class => [ PurchaseOrderEmailActivity::class, + PurchaseOrderEmailedNotification::class, ], PurchaseOrderWasRestored::class => [ PurchaseOrderRestoredActivity::class, @@ -475,8 +478,8 @@ class EventServiceProvider extends ServiceProvider PurchaseOrderViewedActivity::class, ], PurchaseOrderWasAccepted::class => [ + PurchaseOrderAcceptedListener::class, PurchaseOrderAcceptedActivity::class, - PurchaseOrderAcceptedNotification::class, ], CompanyDocumentsDeleted::class => [ DeleteCompanyDocuments::class, diff --git a/app/Services/Bank/BankMatchingService.php b/app/Services/Bank/BankMatchingService.php index 00f1e1183c9f..963e669aef6c 100644 --- a/app/Services/Bank/BankMatchingService.php +++ b/app/Services/Bank/BankMatchingService.php @@ -21,6 +21,7 @@ use App\Models\Invoice; use App\Services\Bank\BankService; use App\Utils\Traits\GeneratesCounter; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -29,7 +30,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; -class BankMatchingService implements ShouldQueue +class BankMatchingService implements ShouldQueue, ShouldBeUnique { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -37,13 +38,10 @@ class BankMatchingService implements ShouldQueue protected $db; - protected $middleware_key; - public function __construct($company_id, $db) { $this->company_id = $company_id; $this->db = $db; - $this->middleware_key = "bank_match_rate:{$this->company_id}"; } public function handle() :void @@ -62,8 +60,14 @@ class BankMatchingService implements ShouldQueue } - public function middleware() + /** + * The unique ID of the job. + * + * @return string + */ + public function uniqueId() { - return [new WithoutOverlapping($this->middleware_key)]; + return (string)$this->company_id; } + } diff --git a/app/Services/ClientPortal/InstantPayment.php b/app/Services/ClientPortal/InstantPayment.php index 2df4ea54fbea..ff496ede5e06 100644 --- a/app/Services/ClientPortal/InstantPayment.php +++ b/app/Services/ClientPortal/InstantPayment.php @@ -48,7 +48,7 @@ class InstantPayment public function run() { - +nlog($this->request->all()); $is_credit_payment = false; $tokens = []; @@ -221,6 +221,9 @@ class InstantPayment if ($this->request->query('hash')) { $hash_data['billing_context'] = Cache::get($this->request->query('hash')); } + elseif($this->request->hash){ + $hash_data['billing_context'] = Cache::get($this->request->hash); + } $payment_hash = new PaymentHash; $payment_hash->hash = Str::random(32); diff --git a/app/Services/Invoice/MarkInvoiceDeleted.php b/app/Services/Invoice/MarkInvoiceDeleted.php index e317c403ade6..4555c86d97e4 100644 --- a/app/Services/Invoice/MarkInvoiceDeleted.php +++ b/app/Services/Invoice/MarkInvoiceDeleted.php @@ -82,9 +82,6 @@ class MarkInvoiceDeleted extends AbstractService { //if total payments = adjustment amount - that means we need to delete the payments as well. -nlog($this->adjustment_amount); -nlog($this->total_payments); - if ($this->adjustment_amount == $this->total_payments) $this->invoice->payments()->update(['payments.deleted_at' => now(), 'payments.is_deleted' => true]); diff --git a/app/Services/Payment/PaymentService.php b/app/Services/Payment/PaymentService.php index 13c8570a44bd..f521ff52a973 100644 --- a/app/Services/Payment/PaymentService.php +++ b/app/Services/Payment/PaymentService.php @@ -140,6 +140,39 @@ class PaymentService return $this; } + public function applyCreditsToInvoice($invoice) + { + + $amount = $invoice->amount; + + $credits = $invoice->client + ->service() + ->getCredits(); + + foreach ($credits as $credit) { + //starting invoice balance + $invoice_balance = $invoice->balance; + + //credit payment applied + $credit->service()->applyPayment($invoice, $amount, $this->payment); + + //amount paid from invoice calculated + $remaining_balance = ($invoice_balance - $invoice->fresh()->balance); + + //reduce the amount to be paid on the invoice from the NEXT credit + $amount -= $remaining_balance; + + //break if the invoice is no longer PAYABLE OR there is no more amount to be applied + if (! $invoice->isPayable() || (int) $amount == 0) { + break; + } + } + + + return $this; + } + + public function save() { $this->payment->saveQuietly(); diff --git a/app/Services/Payment/UpdateInvoicePayment.php b/app/Services/Payment/UpdateInvoicePayment.php index f3cb3debfe6a..0d16ec72e14e 100644 --- a/app/Services/Payment/UpdateInvoicePayment.php +++ b/app/Services/Payment/UpdateInvoicePayment.php @@ -80,11 +80,19 @@ class UpdateInvoicePayment ->clearPartial() ->updateStatus() ->touchPdf() + ->workFlow() ->save(); - $invoice->service() - ->workFlow() - ->save(); + if($invoice->is_proforma) + { + $invoice->number = ''; + $invoice->is_proforma = false; + + $invoice->service() + ->applyNumber() + ->save(); + } + /* Updates the company ledger */ $this->payment @@ -101,17 +109,6 @@ class UpdateInvoicePayment $this->payment->applied += $paid_amount; - $transaction = [ - 'invoice' => $invoice->transaction_event(), - 'payment' => $this->payment->transaction_event(), - 'client' => $client->transaction_event(), - 'credit' => [], - 'metadata' => [], - ]; - - // TransactionLog::dispatch(TransactionEvent::GATEWAY_PAYMENT_MADE, $transaction, $invoice->company->db); - - }); /* Remove the event updater from within the loop to prevent race conditions */ diff --git a/app/Services/PurchaseOrder/MarkSent.php b/app/Services/PurchaseOrder/MarkSent.php index 1450eefc1e41..4ea856ee7358 100644 --- a/app/Services/PurchaseOrder/MarkSent.php +++ b/app/Services/PurchaseOrder/MarkSent.php @@ -40,7 +40,7 @@ class MarkSent ->service() ->setStatus(PurchaseOrder::STATUS_SENT) ->applyNumber() - // ->adjustBalance($this->purchase_order->amount) + ->adjustBalance($this->purchase_order->amount) //why was this commented out previously? // ->touchPdf() ->save(); diff --git a/app/Services/PurchaseOrder/PurchaseOrderExpense.php b/app/Services/PurchaseOrder/PurchaseOrderExpense.php index c24597ce795e..c0c567fc3de7 100644 --- a/app/Services/PurchaseOrder/PurchaseOrderExpense.php +++ b/app/Services/PurchaseOrder/PurchaseOrderExpense.php @@ -39,7 +39,8 @@ class PurchaseOrderExpense $expense->uses_inclusive_taxes = $this->purchase_order->uses_inclusive_taxes; $expense->calculate_tax_by_amount = true; $expense->private_notes = ctrans('texts.purchase_order_number_short') . " " . $this->purchase_order->number; - + $expense->currency_id = $this->purchase_order->vendor->currency_id; + $line_items = $this->purchase_order->line_items; $expense->public_notes = ''; diff --git a/app/Services/PurchaseOrder/PurchaseOrderService.php b/app/Services/PurchaseOrder/PurchaseOrderService.php index 34a18e574894..4457006b908a 100644 --- a/app/Services/PurchaseOrder/PurchaseOrderService.php +++ b/app/Services/PurchaseOrder/PurchaseOrderService.php @@ -97,6 +97,13 @@ class PurchaseOrderService return $this; } + public function adjustBalance($adjustment) + { + $this->purchase_order->balance += $adjustment; + + return $this; + } + public function touchPdf($force = false) { try { diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index f0c64316dffb..05b17e08b085 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -15,6 +15,7 @@ use App\DataMapper\InvoiceItem; use App\Factory\CreditFactory; use App\Factory\InvoiceFactory; use App\Factory\InvoiceToRecurringInvoiceFactory; +use App\Factory\PaymentFactory; use App\Factory\RecurringInvoiceFactory; use App\Jobs\Mail\NinjaMailer; use App\Jobs\Mail\NinjaMailerJob; @@ -28,6 +29,7 @@ use App\Models\ClientContact; use App\Models\Credit; use App\Models\Invoice; use App\Models\PaymentHash; +use App\Models\PaymentType; use App\Models\Product; use App\Models\RecurringInvoice; use App\Models\Subscription; @@ -89,11 +91,18 @@ class SubscriptionService $recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice); $recurring_invoice->auto_bill = $this->subscription->auto_bill; + /* Start the recurring service */ $recurring_invoice->service() ->start() ->save(); + //update the invoice and attach to the recurring invoice!!!!! + $invoice = Invoice::find($payment_hash->fee_invoice_id); + $invoice->recurring_id = $recurring_invoice->id; + $invoice->is_proforma = false; + $invoice->save(); + //execute any webhooks $context = [ 'context' => 'recurring_purchase', @@ -101,7 +110,7 @@ class SubscriptionService 'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id), 'client' => $recurring_invoice->client->hashed_id, 'subscription' => $this->subscription->hashed_id, - 'contact' => auth()->guard('contact')->user() ? auth()->guard('contact')->user()->hashed_id : $recurring_invoice->client->contacts()->first()->hashed_id, + 'contact' => auth()->guard('contact')->user() ? auth()->guard('contact')->user()->hashed_id : $recurring_invoice->client->contacts()->whereNotNull('email')->first()->hashed_id, 'account_key' => $recurring_invoice->client->custom_value2, ]; @@ -217,23 +226,70 @@ class SubscriptionService * * @return float */ + public function calculateUpgradePriceV2(RecurringInvoice $recurring_invoice, Subscription $target) :?float + { + + $outstanding_credit = 0; + + $use_credit_setting = $recurring_invoice->client->getSetting('use_credits_payment'); + + $last_invoice = Invoice::query() + ->where('recurring_id', $recurring_invoice->id) + ->where('is_deleted', 0) + ->where('status_id', Invoice::STATUS_PAID) + ->first(); + + $refund = $this->calculateProRataRefundForSubscription($last_invoice); + + if($use_credit_setting != 'off') + { + + $outstanding_credit = Credit::query() + ->where('client_id', $recurring_invoice->client_id) + ->whereIn('status_id', [Credit::STATUS_SENT,Credit::STATUS_PARTIAL]) + ->where('is_deleted', 0) + ->where('balance', '>', 0) + ->sum('balance'); + + } + + nlog("{$target->price} - {$refund} - {$outstanding_credit}"); + + return $target->price - $refund - $outstanding_credit; + + } + + /** + * Returns an upgrade price when moving between plans + * + * However we only allow people to move between plans + * if their account is in good standing. + * + * @param RecurringInvoice $recurring_invoice + * @param Subscription $target + * @deprecated in favour of calculateUpgradePriceV2 + * @return float + */ public function calculateUpgradePrice(RecurringInvoice $recurring_invoice, Subscription $target) :?float { - //calculate based on daily prices + //calculate based on daily prices $current_amount = $recurring_invoice->amount; $currency_frequency = $recurring_invoice->frequency_id; - $outstanding = $recurring_invoice->invoices() - ->where('is_deleted', 0) - ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) - ->where('balance', '>', 0); + $outstanding = Invoice::query() + ->where('recurring_id', $recurring_invoice->id) + ->where('is_deleted', 0) + ->where('is_proforma',0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('balance', '>', 0); $outstanding_amounts = $outstanding->sum('balance'); - $outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id) - ->where('client_id', $recurring_invoice->client_id) + $outstanding_invoice = Invoice::where('client_id', $recurring_invoice->client_id) ->where('is_deleted', 0) + ->where('is_proforma',0) + ->where('subscription_id', $this->subscription->id) ->orderBy('id', 'desc') ->first(); @@ -242,6 +298,7 @@ class SubscriptionService $outstanding_invoice = Credit::where('subscription_id', $this->subscription->id) ->where('client_id', $recurring_invoice->client_id) + ->where('is_proforma',0) ->where('is_deleted', 0) ->orderBy('id', 'desc') ->first(); @@ -289,13 +346,9 @@ class SubscriptionService $days_in_frequency = $this->getDaysInFrequency(); - $pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $this->subscription->price ,2); + $pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2); - // nlog("days in frequency = {$days_in_frequency} - days of subscription used {$days_of_subscription_used}"); - // nlog("invoice amount = {$invoice->amount}"); - // nlog("pro rata refund = {$pro_rata_refund}"); - - return $pro_rata_refund; + return max(0, $pro_rata_refund); } @@ -323,10 +376,6 @@ class SubscriptionService $pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2); - // nlog("days in frequency = {$days_in_frequency} - days of subscription used {$days_of_subscription_used}"); - // nlog("invoice amount = {$invoice->amount}"); - // nlog("pro rata refund = {$pro_rata_refund}"); - return $pro_rata_refund; } @@ -353,7 +402,6 @@ class SubscriptionService $days_of_subscription_used = $start_date->diffInDays($current_date); - // $days_in_frequency = $this->getDaysInFrequency(); $days_in_frequency = $invoice->subscription->service()->getDaysInFrequency(); $ratio = ($days_in_frequency - $days_of_subscription_used)/$days_in_frequency; @@ -406,10 +454,76 @@ class SubscriptionService return $pro_rata_charge; } + /** + * This entry point assumes the user does not have to make a + * payment for the service. + * + * In this case, we generate a credit note for the old service + * Generate a new invoice for the new service + * Apply credits to the invoice + * + * @param array $data + */ + public function createChangePlanCreditV2($data) + { + /* Init vars */ + $recurring_invoice = $data['recurring_invoice']; + $old_subscription = $data['subscription']; + $target_subscription = $data['target']; + + $pro_rata_charge_amount = 0; + $pro_rata_refund_amount = 0; + $is_credit = false; + $credit = false; + + /* Get last invoice */ + $last_invoice = Invoice::where('subscription_id', $recurring_invoice->subscription_id) + ->where('client_id', $recurring_invoice->client_id) + ->where('is_proforma',0) + ->where('is_deleted', 0) + ->where('status_id', Invoice::STATUS_PAID) + ->withTrashed() + ->orderBy('id', 'desc') + ->first(); + + if($this->calculateProRataRefundForSubscription($last_invoice) > 0) + $credit = $this->createCredit($last_invoice, $target_subscription, false); + + $new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice); + + $invoice = $this->changePlanInvoice($target_subscription, $recurring_invoice->client_id); + $invoice->recurring_id = $new_recurring_invoice->id; + $invoice->save(); + + $payment = PaymentFactory::create($invoice->company_id, $invoice->user_id, $invoice->client_id); + $payment->type_id = PaymentType::CREDIT; + $payment->client_id = $invoice->client_id; + $payment->is_manual = true; + $payment->save(); + + $payment->service()->applyCreditsToInvoice($invoice); + + $context = [ + 'context' => 'change_plan', + 'recurring_invoice' => $new_recurring_invoice->hashed_id, + 'credit' => $credit ? $credit->hashed_id : null, + 'client' => $new_recurring_invoice->client->hashed_id, + 'subscription' => $target_subscription->hashed_id, + 'contact' => auth()->guard('contact')->user()->hashed_id, + 'account_key' => $new_recurring_invoice->client->custom_value2, + ]; + + $response = $this->triggerWebhook($context); + + return '/client/recurring_invoices/'.$new_recurring_invoice->hashed_id; + + } + /** * When downgrading, we may need to create * a credit * + * @deprecated in favour of createChangePlanCreditV2 * @param array $data */ public function createChangePlanCredit($data) @@ -663,10 +777,10 @@ class SubscriptionService $credit = CreditFactory::create($this->subscription->company_id, $this->subscription->user_id); $credit->date = now()->format('Y-m-d'); $credit->subscription_id = $this->subscription->id; - - $line_items = $subscription_repo->generateLineItems($target, false, true); - - $credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, $last_invoice_is_credit)); + $credit->discount = $last_invoice->discount; + $credit->is_amount_discount = $last_invoice->is_amount_discount; + + $credit->line_items = $this->calculateProRataRefundItems($last_invoice, true); $data = [ 'client_id' => $last_invoice->client_id, @@ -696,6 +810,7 @@ class SubscriptionService $invoice->subscription_id = $target->id; $invoice->line_items = array_merge($subscription_repo->generateLineItems($target), $this->calculateProRataRefundItems($last_invoice)); + $invoice->is_proforma = true; $data = [ 'client_id' => $client_id, @@ -711,6 +826,40 @@ class SubscriptionService } + /** + * When changing plans we need to generate a pro rata + * invoice which takes into account any credits. + * + * @param Subscription $target + * @return Invoice + */ + private function changePlanInvoice($target, $client_id) + { + $subscription_repo = new SubscriptionRepository(); + $invoice_repo = new InvoiceRepository(); + + $invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); + $invoice->date = now()->format('Y-m-d'); + $invoice->subscription_id = $target->id; + + $invoice->line_items = $subscription_repo->generateLineItems($target); + $invoice->is_proforma = true; + + $data = [ + 'client_id' => $client_id, + 'quantity' => 1, + 'date' => now()->format('Y-m-d'), + ]; + + return $invoice_repo->save($data, $invoice) + ->service() + ->markSent() + ->fillDefaults() + ->save(); + + } + + public function createInvoiceV2($bundle, $client_id, $valid_coupon = false) { @@ -720,7 +869,8 @@ class SubscriptionService $invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); $invoice->subscription_id = $this->subscription->id; $invoice->client_id = $client_id; - + $invoice->is_proforma = true; + $invoice->number = ctrans('texts.subscription') . "_" . now()->format('Y-m-d') . "_" . rand(0,100000); $line_items = $bundle->map(function ($item){ $line_item = new InvoiceItem; @@ -760,6 +910,7 @@ class SubscriptionService $invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); $invoice->line_items = $subscription_repo->generateLineItems($this->subscription); $invoice->subscription_id = $this->subscription->id; + $invoice->is_proforman = true; if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { @@ -771,7 +922,6 @@ class SubscriptionService $invoice->is_amount_discount = $this->subscription->is_amount_discount; } - return $invoice_repo->save($data, $invoice); } @@ -860,14 +1010,11 @@ class SubscriptionService */ public function triggerWebhook($context) { - nlog("trigger webhook"); if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) { return ["message" => "Success", "status_code" => 200]; } - nlog("past first if"); - $response = false; $body = array_merge($context, [ @@ -876,8 +1023,6 @@ class SubscriptionService $response = $this->sendLoad($this->subscription, $body); - nlog("after response"); - /* Append the response to the system logger body */ if(is_array($response)){ @@ -1098,8 +1243,6 @@ class SubscriptionService }); - - return $this->handleRedirect('client/subscriptions'); } diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 8ee24b227783..2f20c5300617 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -193,6 +193,7 @@ class CompanyTransformer extends EntityTransformer 'invoice_task_lock' => (bool) $company->invoice_task_lock, 'convert_payment_currency' => (bool) $company->convert_payment_currency, 'convert_expense_currency' => (bool) $company->convert_expense_currency, + 'notify_vendor_when_paid' => (bool) $company->notify_vendor_when_paid, ]; } diff --git a/app/Transformers/RecurringExpenseTransformer.php b/app/Transformers/RecurringExpenseTransformer.php index 5830f7a2ad5d..6056b91d37c0 100644 --- a/app/Transformers/RecurringExpenseTransformer.php +++ b/app/Transformers/RecurringExpenseTransformer.php @@ -15,6 +15,7 @@ use App\Models\Document; use App\Models\RecurringExpense; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\SoftDeletes; +use League\Fractal\Resource\Item; /** * class RecurringExpenseTransformer. @@ -33,6 +34,8 @@ class RecurringExpenseTransformer extends EntityTransformer */ protected $availableIncludes = [ 'documents', + 'client', + 'vendor', ]; public function includeDocuments(RecurringExpense $recurring_expense) @@ -42,6 +45,28 @@ class RecurringExpenseTransformer extends EntityTransformer return $this->includeCollection($recurring_expense->documents, $transformer, Document::class); } + public function includeClient(RecurringExpense $recurring_expense): ?Item + { + $transformer = new ClientTransformer($this->serializer); + + if (!$recurring_expense->client) { + return null; + } + + return $this->includeItem($recurring_expense->client, $transformer, Client::class); + } + + public function includeVendor(RecurringExpense $recurring_expense): ?Item + { + $transformer = new VendorTransformer($this->serializer); + + if (!$recurring_expense->vendor) { + return null; + } + + return $this->includeItem($recurring_expense->vendor, $transformer, Vendor::class); + } + /** * @param RecurringExpense $recurring_expense * diff --git a/app/Utils/Helpers.php b/app/Utils/Helpers.php index 8664e4654baa..e2797049b2b3 100644 --- a/app/Utils/Helpers.php +++ b/app/Utils/Helpers.php @@ -129,12 +129,12 @@ class Helpers if(!$string_hit) return $value; - // 04-10-2022 Return Early if no reserved keywords are present, this is a very expensive process + // 04-10-2022 Return Early if no reserved keywords are present, this is a very expensive process Carbon::setLocale($entity->locale()); if (!$currentDateTime) { - $currentDateTime = Carbon::now(); + $currentDateTime = Carbon::now()->timezone($entity->timezone()->name); } $replacements = [ diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index 1ea15dde0c25..2aacaf39f469 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -66,7 +66,8 @@ trait GeneratesCounter $counter = 1; } - $counter_entity = $client->group_settings; +// $counter_entity = $client->group_settings; + $counter_entity = $client->group_settings ?: $client->company; } else { $counter = $client->company->settings->{$counter_string}; $counter_entity = $client->company; diff --git a/app/Utils/Traits/Pdf/PDF.php b/app/Utils/Traits/Pdf/PDF.php index f81407b9f9c0..a3f0727f714f 100644 --- a/app/Utils/Traits/Pdf/PDF.php +++ b/app/Utils/Traits/Pdf/PDF.php @@ -25,7 +25,7 @@ class PDF extends FPDI $this->SetTextColor(135, 135, 135); $trans = ctrans('texts.pdf_page_info', ['current' => $this->PageNo(), 'total' => '{nb}']); - $trans = iconv('UTF-8', 'ISO-8859-7', $trans); + // $trans = iconv('UTF-8', 'ISO-8859-7', $trans); $this->Cell(0, 5, $trans, 0, 0, $this->text_alignment); } diff --git a/app/Utils/Traits/SettingsSaver.php b/app/Utils/Traits/SettingsSaver.php index 4e6216a254c8..05dee87c946b 100644 --- a/app/Utils/Traits/SettingsSaver.php +++ b/app/Utils/Traits/SettingsSaver.php @@ -52,7 +52,7 @@ trait SettingsSaver continue; } /*Separate loop if it is a _id field which is an integer cast as a string*/ - elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && property_exists($settings, $key) && strlen($settings->{$key}) >= 1)) { + elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && property_exists($settings, $key) && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && property_exists($settings, $key) && strlen($settings->{$key}) >= 1)) { $value = 'integer'; if($key == 'gmail_sending_user_id' || $key == 'besr_id') diff --git a/app/Utils/Traits/SubscriptionHooker.php b/app/Utils/Traits/SubscriptionHooker.php index dc13aa491aaa..cd4d73a8525b 100644 --- a/app/Utils/Traits/SubscriptionHooker.php +++ b/app/Utils/Traits/SubscriptionHooker.php @@ -12,6 +12,8 @@ namespace App\Utils\Traits; use GuzzleHttp\RequestOptions; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Psr7\Message; /** * Class SubscriptionHooker. @@ -34,10 +36,6 @@ trait SubscriptionHooker 'headers' => $headers, ]); - nlog('method name must be a string'); - nlog($subscription->webhook_configuration['post_purchase_rest_method']); - nlog($subscription->webhook_configuration['post_purchase_url']); - $post_purchase_rest_method = (string) $subscription->webhook_configuration['post_purchase_rest_method']; $post_purchase_url = (string) $subscription->webhook_configuration['post_purchase_url']; @@ -47,7 +45,18 @@ trait SubscriptionHooker ]); return array_merge($body, json_decode($response->getBody(), true)); - } catch (\Exception $e) { + } catch (ClientException $e) { + + $message = $e->getMessage(); + + $error = json_decode($e->getResponse()->getBody()->getContents()); + + if(property_exists($error, 'message')) + $message = $error->message; + + return array_merge($body, ['message' => $message, 'status_code' => 500]); + } + catch (\Exception $e) { return array_merge($body, ['message' => $e->getMessage(), 'status_code' => 500]); } } diff --git a/config/ninja.php b/config/ninja.php index 845ef8659658..571db89cd4c2 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.5.49', - 'app_tag' => '5.5.49', + 'app_version' => '5.5.50', + 'app_tag' => '5.5.50', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), @@ -190,7 +190,8 @@ return [ 'ninja_stripe_client_id' => env('NINJA_STRIPE_CLIENT_ID', null), 'ninja_default_company_id' => env('NINJA_COMPANY_ID', null), 'ninja_default_company_gateway_id' => env('NINJA_COMPANY_GATEWAY_ID', null), - 'ninja_hosted_secret' => env('NINJA_HOSTED_SECRET', null), + 'ninja_hosted_secret' => env('NINJA_HOSTED_SECRET', ''), + 'ninja_hosted_header' =>env('NINJA_HEADER',''), 'internal_queue_enabled' => env('INTERNAL_QUEUE_ENABLED', true), 'ninja_apple_api_key' => env('APPLE_API_KEY', false), 'ninja_apple_private_key' => env('APPLE_PRIVATE_KEY', false), diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php index ecba3066cd16..7f8f25833838 100644 --- a/database/factories/ProductFactory.php +++ b/database/factories/ProductFactory.php @@ -29,14 +29,8 @@ class ProductFactory extends Factory 'cost' => $this->faker->numberBetween(1, 1000), 'price' => $this->faker->numberBetween(1, 1000), 'quantity' => $this->faker->numberBetween(1, 100), - // 'tax_name1' => 'GST', - // 'tax_rate1' => 10, - // 'tax_name2' => 'VAT', - // 'tax_rate2' => 17.5, - // 'tax_name3' => 'THIRDTAX', - // 'tax_rate3' => 5, - 'custom_value1' => $this->faker->text(20), - 'custom_value2' => $this->faker->text(20), + 'custom_value1' => 'https://picsum.photos/200', + 'custom_value2' => rand(0,100), 'custom_value3' => $this->faker->text(20), 'custom_value4' => $this->faker->text(20), 'is_deleted' => false, diff --git a/database/migrations/2022_12_20_063038_set_proforma_invoice_type.php b/database/migrations/2022_12_20_063038_set_proforma_invoice_type.php new file mode 100644 index 000000000000..28dae37d58a9 --- /dev/null +++ b/database/migrations/2022_12_20_063038_set_proforma_invoice_type.php @@ -0,0 +1,38 @@ +boolean('notify_vendor_when_paid')->default(false); + }); + + Schema::table('invoices', function (Blueprint $table) + { + $table->boolean('is_proforma')->default(false); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index 03e365511598..52d17e7a389a 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4301,7 +4301,7 @@ $LANG = array( 'becs_mandate' => 'By providing your bank account details, you agree to this Direct Debit Request and the Direct Debit Request service agreement, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.', 'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.', 'direct_debit' => 'Direct Debit', - 'clone_to_expense' => 'Clone to expense', + 'clone_to_expense' => 'Clone to Expense', 'checkout' => 'Checkout', 'acss' => 'Pre-authorized debit payments', 'invalid_amount' => 'Invalid amount. Number/Decimal values only.', @@ -4906,7 +4906,12 @@ $LANG = array( 'backup_restore' => 'Backup | Restore', 'export_company' => 'Create company backup', 'backup' => 'Backup', - + 'notification_purchase_order_created_body' => 'The following purchase_order :purchase_order was created for vendor :vendor for :amount.', + 'notification_purchase_order_created_subject' => 'Purchase Order :purchase_order was created for :vendor', + 'notification_purchase_order_sent_subject' => 'Purchase Order :purchase_order was sent to :vendor', + 'notification_purchase_order_sent' => 'The following vendor :vendor was emailed Purchase Order :purchase_order for :amount.', + 'subscription_blocked' => 'This product is a restricted item, please contact the vendor for further information.', + 'subscription_blocked_title' => 'Product not available.', ); return $LANG; diff --git a/resources/views/billing-portal/purchase.blade.php b/resources/views/billing-portal/purchase.blade.php index 80fe64c74ac7..5ad20943cc96 100644 --- a/resources/views/billing-portal/purchase.blade.php +++ b/resources/views/billing-portal/purchase.blade.php @@ -2,7 +2,7 @@ @section('meta_title', ctrans('texts.purchase')) @section('body') - @livewire('billing-portal-purchase', ['subscription' => $subscription, 'company' => $subscription->company, 'contact' => auth()->guard('contact')->user(), 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null]) + @livewire('billing-portal-purchase', ['subscription' => $subscription->id, 'db' => $subscription->company->db, 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null]) @stop @push('footer') diff --git a/resources/views/billing-portal/purchasev2.blade.php b/resources/views/billing-portal/purchasev2.blade.php index 91c557dc222b..239d2a271f99 100644 --- a/resources/views/billing-portal/purchasev2.blade.php +++ b/resources/views/billing-portal/purchasev2.blade.php @@ -2,7 +2,7 @@ @section('meta_title', ctrans('texts.purchase')) @section('body') - @livewire('billing-portal-purchasev2', ['subscription' => $subscription, 'company' => $subscription->company, 'contact' => auth()->guard('contact')->user(), 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null]) + @livewire('billing-portal-purchasev2', ['subscription' => $subscription->id, 'db' => $subscription->company->db, 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null]) @stop @push('footer') diff --git a/resources/views/portal/ninja2020/auth/register.blade.php b/resources/views/portal/ninja2020/auth/register.blade.php index 84e2c0f2a94e..34b78f406de4 100644 --- a/resources/views/portal/ninja2020/auth/register.blade.php +++ b/resources/views/portal/ninja2020/auth/register.blade.php @@ -5,13 +5,21 @@
+ @if($register_company->account && !$register_company->account->isPaid())
- {{ ctrans('texts.logo') }} -
+ Invoice Ninja logo +
+ @elseif(isset($register_company) && !is_null($register_company)) +
+ {{ $register_company->present()->name() }} logo +
+ @endif

{{ ctrans('texts.register') }}

{{ ctrans('texts.register_label') }}

-
+ @if($register_company) @endif @@ -54,6 +62,18 @@ type="password" name="{{ $field['key'] }}" /> + @elseif($field['key'] === 'currency_id') + @elseif($field['key'] === 'country_id') @@ -128,8 +151,10 @@ @enderror - - + +
diff --git a/resources/views/portal/ninja2020/components/general/card-element.blade.php b/resources/views/portal/ninja2020/components/general/card-element.blade.php index b7b52898779c..c97949137de6 100644 --- a/resources/views/portal/ninja2020/components/general/card-element.blade.php +++ b/resources/views/portal/ninja2020/components/general/card-element.blade.php @@ -1,4 +1,4 @@ -
+
{{ $title }}
diff --git a/resources/views/portal/ninja2020/components/livewire/billing-portal-purchasev2.blade.php b/resources/views/portal/ninja2020/components/livewire/billing-portal-purchasev2.blade.php index 4f66491104a3..6c0ff64b36c7 100644 --- a/resources/views/portal/ninja2020/components/livewire/billing-portal-purchasev2.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/billing-portal-purchasev2.blade.php @@ -196,6 +196,13 @@ @endforeach @endif + @if(auth()->guard('contact')->check()) +
  • + +
  • + @endif
    @@ -209,7 +216,7 @@ @foreach($bundle->toArray() as $item)
    - {{$item['product']}} x {{$item['qty']}} + {{ $item['qty'] }} x {{ substr(str_replace(["\r","\n","
    ","
    ","
    ","
    "]," ", $item['product']), 0, 30) . "..." }}
    {{ $item['price'] }}
    @endforeach @@ -284,6 +291,7 @@ @endif + @if($is_eligible)
    + @else + {{ $this->not_eligible_message }} + @endif diff --git a/resources/views/portal/ninja2020/credits/index.blade.php b/resources/views/portal/ninja2020/credits/index.blade.php index b485339c7a2e..c5b5e43c896c 100644 --- a/resources/views/portal/ninja2020/credits/index.blade.php +++ b/resources/views/portal/ninja2020/credits/index.blade.php @@ -13,6 +13,6 @@ @section('body')
    - @livewire('credits-table', ['company' => $company]) + @livewire('credits-table', ['company_id' => $company->id, 'db' => $company->db])
    @endsection \ No newline at end of file diff --git a/resources/views/portal/ninja2020/credits/show.blade.php b/resources/views/portal/ninja2020/credits/show.blade.php index 0d802adc7d79..c8cccc0f7c82 100644 --- a/resources/views/portal/ninja2020/credits/show.blade.php +++ b/resources/views/portal/ninja2020/credits/show.blade.php @@ -34,7 +34,6 @@ @include('portal.ninja2020.components.entity-documents', ['entity' => $credit]) @include('portal.ninja2020.components.pdf-viewer', ['entity' => $credit, 'invitation' => $invitation]) - @endsection @@ -45,18 +44,5 @@ var clipboard = new ClipboardJS('.btn'); - // clipboard.on('success', function(e) { - // console.info('Action:', e.action); - // console.info('Text:', e.text); - // console.info('Trigger:', e.trigger); - - // e.clearSelection(); - // }); - - // clipboard.on('error', function(e) { - // console.error('Action:', e.action); - // console.error('Trigger:', e.trigger); - // }); - @endsection diff --git a/resources/views/portal/ninja2020/documents/index.blade.php b/resources/views/portal/ninja2020/documents/index.blade.php index 61fc9a649808..09d40bcb2aac 100644 --- a/resources/views/portal/ninja2020/documents/index.blade.php +++ b/resources/views/portal/ninja2020/documents/index.blade.php @@ -14,5 +14,5 @@ @csrf - @livewire('documents-table', ['client' => $client, 'company' => $company]) + @livewire('documents-table', ['client_id' => $client->id, 'db' => $company->db]) @endsection diff --git a/resources/views/portal/ninja2020/gateways/credit/index.blade.php b/resources/views/portal/ninja2020/gateways/credit/index.blade.php index d0176f9d136f..df489d903777 100644 --- a/resources/views/portal/ninja2020/gateways/credit/index.blade.php +++ b/resources/views/portal/ninja2020/gateways/credit/index.blade.php @@ -5,6 +5,7 @@
    @csrf +
    diff --git a/resources/views/portal/ninja2020/generic/subscription_blocked.blade.php b/resources/views/portal/ninja2020/generic/subscription_blocked.blade.php new file mode 100644 index 000000000000..103fbdb75abe --- /dev/null +++ b/resources/views/portal/ninja2020/generic/subscription_blocked.blade.php @@ -0,0 +1,31 @@ +@extends('portal.ninja2020.layout.clean') +@section('meta_title', ctrans('texts.error')) + +@section('body') + +
    +
    +
    + + @if($account && !$account->isPaid()) +
    + Invoice Ninja logo +
    + @elseif(isset($company) && !is_null($company)) +
    + {{ $company->present()->name() }} logo +
    + @endif +

    {{ ctrans("texts.subscription_blocked_title") }}

    +

    {{ ctrans('texts.subscription_blocked') }}

    +
    +
    +
    + +@stop + +@push('footer') + +@endpush \ No newline at end of file diff --git a/resources/views/portal/ninja2020/invoices/index.blade.php b/resources/views/portal/ninja2020/invoices/index.blade.php index 3d23cb6bc33f..0ce6e4292b7b 100644 --- a/resources/views/portal/ninja2020/invoices/index.blade.php +++ b/resources/views/portal/ninja2020/invoices/index.blade.php @@ -23,6 +23,6 @@
    - @livewire('invoices-table', ['company' => $company]) + @livewire('invoices-table', ['company_id' => $company->id, 'db' => $company->db])
    @endsection diff --git a/resources/views/portal/ninja2020/invoices/show.blade.php b/resources/views/portal/ninja2020/invoices/show.blade.php index 6fb066a2ee99..c52b7aba16cd 100644 --- a/resources/views/portal/ninja2020/invoices/show.blade.php +++ b/resources/views/portal/ninja2020/invoices/show.blade.php @@ -31,7 +31,7 @@ - + diff --git a/resources/views/portal/ninja2020/payment_methods/index.blade.php b/resources/views/portal/ninja2020/payment_methods/index.blade.php index 60d72006081e..8bdde30840f7 100644 --- a/resources/views/portal/ninja2020/payment_methods/index.blade.php +++ b/resources/views/portal/ninja2020/payment_methods/index.blade.php @@ -3,6 +3,6 @@ @section('body')
    - @livewire('payment-methods-table', ['client' => $client, 'company' => $company]) + @livewire('payment-methods-table', ['client_id' => $client->id, 'db' => $company->db])
    @endsection diff --git a/resources/views/portal/ninja2020/payments/index.blade.php b/resources/views/portal/ninja2020/payments/index.blade.php index 22bc5cccf64a..ad595d213764 100644 --- a/resources/views/portal/ninja2020/payments/index.blade.php +++ b/resources/views/portal/ninja2020/payments/index.blade.php @@ -3,6 +3,6 @@ @section('body')
    - @livewire('payments-table', ['company' => $company]) + @livewire('payments-table', ['company_id' => $company->id, 'db' => $company->db])
    @endsection \ No newline at end of file diff --git a/resources/views/portal/ninja2020/quotes/index.blade.php b/resources/views/portal/ninja2020/quotes/index.blade.php index 078e23ef7670..b3b8e279bb07 100644 --- a/resources/views/portal/ninja2020/quotes/index.blade.php +++ b/resources/views/portal/ninja2020/quotes/index.blade.php @@ -26,6 +26,6 @@
    - @livewire('quotes-table', ['company' => $company]) + @livewire('quotes-table', ['company_id' => $company->id, 'db' => $company->db])
    @endsection diff --git a/routes/client.php b/routes/client.php index 207c05015fdf..f322850ab1b0 100644 --- a/routes/client.php +++ b/routes/client.php @@ -54,7 +54,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie Route::post('invoices/payment', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'bulk'])->name('invoices.bulk'); Route::get('invoices/payment', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'catch_bulk'])->name('invoices.catch_bulk'); Route::post('invoices/download', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'download'])->name('invoices.download'); - Route::get('invoices/{invoice}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show'); + Route::get('invoices/{invoice}/{hash?}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show'); Route::get('invoices/{invoice_invitation}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show_invitation'); Route::get('recurring_invoices', [App\Http\Controllers\ClientPortal\RecurringInvoiceController::class, 'index'])->name('recurring_invoices.index')->middleware('portal_enabled'); diff --git a/tests/Feature/ClientPortal/CreditsTest.php b/tests/Feature/ClientPortal/CreditsTest.php index 178a766b6b2e..a579a9fd1bf1 100644 --- a/tests/Feature/ClientPortal/CreditsTest.php +++ b/tests/Feature/ClientPortal/CreditsTest.php @@ -100,7 +100,7 @@ class CreditsTest extends TestCase $c2->load('client'); $c3->load('client'); - Livewire::test(CreditsTable::class, ['company' => $company]) + Livewire::test(CreditsTable::class, ['company_id' => $company->id, 'db' => $company->db]) ->assertDontSee('testing-number-01') ->assertSee('testing-number-02') ->assertSee('testing-number-03'); @@ -167,7 +167,7 @@ class CreditsTest extends TestCase $this->actingAs($client->contacts->first(), 'contact'); - Livewire::test(CreditsTable::class, ['company' => $company]) + Livewire::test(CreditsTable::class, ['company_id' => $company->id, 'db' => $company->db]) ->assertSee('testing-number-01') ->assertSee('testing-number-02') ->assertSee('testing-number-03'); diff --git a/tests/Feature/ClientPortal/InvoicesTest.php b/tests/Feature/ClientPortal/InvoicesTest.php index 084961223df3..aa606e1f05d0 100644 --- a/tests/Feature/ClientPortal/InvoicesTest.php +++ b/tests/Feature/ClientPortal/InvoicesTest.php @@ -86,12 +86,12 @@ class InvoicesTest extends TestCase $this->actingAs($client->contacts->first(), 'contact'); - Livewire::test(InvoicesTable::class, ['company' => $company]) + Livewire::test(InvoicesTable::class, ['company_id' => $company->id, 'db' => $company->db]) ->assertSee($sent->number) ->assertSee($paid->number) ->assertSee($unpaid->number); - Livewire::test(InvoicesTable::class, ['company' => $company]) + Livewire::test(InvoicesTable::class, ['company_id' => $company->id, 'db' => $company->db]) ->set('status', ['paid']) ->assertSee($paid->number) ->assertDontSee($unpaid->number); diff --git a/tests/Feature/Export/ProfitAndLossReportTest.php b/tests/Feature/Export/ProfitAndLossReportTest.php index 2c81becab4b0..d052b9f091e5 100644 --- a/tests/Feature/Export/ProfitAndLossReportTest.php +++ b/tests/Feature/Export/ProfitAndLossReportTest.php @@ -144,7 +144,7 @@ class ProfitAndLossReportTest extends TestCase 'balance' => 11, 'status_id' => 2, 'total_taxes' => 1, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'terms' => 'nada', 'discount' => 0, 'tax_rate1' => 0, @@ -183,7 +183,7 @@ class ProfitAndLossReportTest extends TestCase 'balance' => 10, 'status_id' => 2, 'total_taxes' => 1, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'terms' => 'nada', 'discount' => 0, 'tax_rate1' => 10, @@ -226,7 +226,7 @@ class ProfitAndLossReportTest extends TestCase 'balance' => 10, 'status_id' => 2, 'total_taxes' => 1, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'terms' => 'nada', 'discount' => 0, 'tax_rate1' => 10, @@ -282,7 +282,7 @@ class ProfitAndLossReportTest extends TestCase 'balance' => 10, 'status_id' => 2, 'total_taxes' => 0, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'terms' => 'nada', 'discount' => 0, 'tax_rate1' => 0, @@ -313,7 +313,7 @@ class ProfitAndLossReportTest extends TestCase 'amount' => 10, 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), ]); $pl = new ProfitLoss($this->company, $this->payload); @@ -334,7 +334,7 @@ class ProfitAndLossReportTest extends TestCase $e = ExpenseFactory::create($this->company->id, $this->user->id); $e->amount = 10; - $e->date = '2022-01-01'; + $e->date = now()->format('Y-m-d'); $e->calculate_tax_by_amount = true; $e->tax_amount1 = 10; $e->save(); @@ -358,7 +358,7 @@ class ProfitAndLossReportTest extends TestCase $e = ExpenseFactory::create($this->company->id, $this->user->id); $e->amount = 10; - $e->date = '2022-01-01'; + $e->date = now()->format('Y-m-d'); $e->tax_rate1 = 10; $e->tax_name1 = 'GST'; $e->uses_inclusive_taxes = false; @@ -383,7 +383,7 @@ class ProfitAndLossReportTest extends TestCase $e = ExpenseFactory::create($this->company->id, $this->user->id); $e->amount = 10; - $e->date = '2022-01-01'; + $e->date = now()->format('Y-m-d'); $e->tax_rate1 = 10; $e->tax_name1 = 'GST'; $e->uses_inclusive_taxes = false; @@ -410,7 +410,7 @@ class ProfitAndLossReportTest extends TestCase 'amount' => 10, 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'exchange_rate' => 1, 'currency_id' => $this->company->settings->currency_id, ]); @@ -440,7 +440,7 @@ class ProfitAndLossReportTest extends TestCase 'amount' => 10, 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'exchange_rate' => 1, 'currency_id' => $this->company->settings->currency_id, ]); @@ -454,7 +454,7 @@ class ProfitAndLossReportTest extends TestCase 'amount' => 10, 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'exchange_rate' => 1, 'currency_id' => $this->company->settings->currency_id, ]); @@ -489,7 +489,7 @@ class ProfitAndLossReportTest extends TestCase 'balance' => 10, 'status_id' => 2, 'total_taxes' => 1, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'terms' => 'nada', 'discount' => 0, 'tax_rate1' => 10, @@ -510,7 +510,7 @@ class ProfitAndLossReportTest extends TestCase 'amount' => 10, 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'exchange_rate' => 1, 'currency_id' => $this->company->settings->currency_id, ]); @@ -524,7 +524,7 @@ class ProfitAndLossReportTest extends TestCase 'amount' => 10, 'company_id' => $this->company->id, 'user_id' => $this->user->id, - 'date' => '2022-01-01', + 'date' => now()->format('Y-m-d'), 'exchange_rate' => 1, 'currency_id' => $this->company->settings->currency_id, ]); diff --git a/tests/Unit/DatesTest.php b/tests/Unit/DatesTest.php index 5aa3e2c40e06..5045414bc450 100644 --- a/tests/Unit/DatesTest.php +++ b/tests/Unit/DatesTest.php @@ -70,4 +70,14 @@ class DatesTest extends TestCase $this->assertFalse($date_in_future->gt(Carbon::parse($date_in_past)->addDays(14))); } + + /*Test time travelling behaves as expected */ + // public function testTimezoneShifts() + // { + // $this->travel(Carbon::parse('2022-12-20')); + + // $this->assertEquals('2022-12-20', now()->setTimeZone('Pacific/Midway')->format('Y-m-d')); + + // $this->travelBack(); + // } } diff --git a/tests/Unit/GeneratesConvertedQuoteCounterTest.php b/tests/Unit/GeneratesConvertedQuoteCounterTest.php index 5c6e701b3f28..f0d9e53cc373 100644 --- a/tests/Unit/GeneratesConvertedQuoteCounterTest.php +++ b/tests/Unit/GeneratesConvertedQuoteCounterTest.php @@ -115,8 +115,8 @@ class GeneratesConvertedQuoteCounterTest extends TestCase $this->assertNotNull($invoice); - $this->assertEquals('2022-Q0001', $quote->number); - $this->assertEquals('2022-I0001', $invoice->number); + $this->assertEquals(now()->format('Y'). '-Q0001', $quote->number); + $this->assertEquals(now()->format('Y'). '-I0001', $invoice->number); $settings = $this->client->getMergedSettings(); $settings->invoice_number_counter = 100;