diff --git a/VERSION.txt b/VERSION.txt index fd2784a84..6ca7fd49a 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 cc236aac3..6033dabcd 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 147ae4ad5..2d6c983ae 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 9beaefda8..3c5fd83d6 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 5e6397267..505e40742 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 8bcfc55f1..a993cb2aa 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 422842a46..cd1945399 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 410da1987..8d4128535 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 2d9f957d7..77fd4ede6 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 895b75b4e..799117214 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 5446c0532..116e3ca53 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 db7fc1e0c..c7d24fb26 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 622b64d20..607c7a27c 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 6e3e72bb3..9267853d0 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 b5b4593a5..aa829ef45 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 fb569df26..04354e228 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 114b61016..23e5208ee 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 83d6be93e..f8780baa2 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 47cbcd68a..452c58953 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 3433f07a5..404194f58 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 dd2bf843e..8ff7b6114 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 b347e6486..afdc66545 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 e5748d80e..60b3b761d 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 f35715e17..3bfaa9390 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 d0b1e1190..077e78b05 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 881d30afd..d0f85db91 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 208f3cc54..1ad99fa2b 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 93f416703..da6785e82 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 aac881662..47e4ce5a0 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 456bb3cf9..a4be814fc 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 26a0aa825..b795ee609 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 79934b238..ab9181a31 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 a36369f01..69d297127 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 348cad69d..05dae8421 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 ef256795c..7bc220a89 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 0909c775c..37540310c 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 000000000..6517ea331 --- /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 d65681cc5..ff64295ee 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 000000000..088aaf81c --- /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 000000000..36cc20352 --- /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 abbafc457..0c69cc073 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 38555b06d..2cec34303 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 b960270ad..fe636efc5 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 36daf77e0..7864220ca 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 24fcf324f..abde54bf7 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 5c79eb531..e17e3f648 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 1ada04574..b6a05d989 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 d9d2ff4b3..7ac9601d5 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 c6a4f9fdb..dee640c44 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 cc3a02d52..2e7b59ebc 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 00f1e1183..963e669ae 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 2df4ea54f..ff496ede5 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 e317c403a..4555c86d9 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 13c8570a4..f521ff52a 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 f3cb3debf..0d16ec72e 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 1450eefc1..4ea856ee7 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 c24597ce7..c0c567fc3 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 34a18e574..4457006b9 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 f0c64316d..05b17e08b 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 8ee24b227..2f20c5300 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 5830f7a2a..6056b91d3 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 8664e4654..e2797049b 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 1ea15dde0..2aacaf39f 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 f81407b9f..a3f0727f7 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 4e6216a25..05dee87c9 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 dc13aa491..cd4d73a85 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 845ef8659..571db89cd 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 ecba3066c..7f8f25833 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 000000000..28dae37d5 --- /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 03e365511..52d17e7a3 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 80fe64c74..5ad20943c 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 91c557dc2..239d2a271 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 84e2c0f2a..34b78f406 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 b7b528987..c97949137 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 4f6649110..6c0ff64b3 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 b485339c7..c5b5e43c8 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 0d802adc7..c8cccc0f7 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 61fc9a649..09d40bcb2 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 d0176f9d1..df489d903 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 000000000..103fbdb75 --- /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 3d23cb6bc..0ce6e4292 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 6fb066a2e..c52b7aba1 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 60d720060..8bdde3084 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 22bc5cccf..ad595d213 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 078e23ef7..b3b8e279b 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 207c05015..f322850ab 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 178a766b6..a579a9fd1 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 084961223..aa606e1f0 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 2c81becab..d052b9f09 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 5aa3e2c40..5045414bc 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 5c6e701b3..f0d9e53cc 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;