diff --git a/VERSION.txt b/VERSION.txt index 4b336d6ed820..1e20ec35c642 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.3.100 \ No newline at end of file +5.4.0 \ No newline at end of file diff --git a/app/Events/PurchaseOrder/PurchaseOrderWasAccepted.php b/app/Events/PurchaseOrder/PurchaseOrderWasAccepted.php new file mode 100644 index 000000000000..ed2147b5fdb1 --- /dev/null +++ b/app/Events/PurchaseOrder/PurchaseOrderWasAccepted.php @@ -0,0 +1,51 @@ +purchase_order = $purchase_order; + $this->contact = $contact; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/PurchaseOrder/PurchaseOrderWasViewed.php b/app/Events/PurchaseOrder/PurchaseOrderWasViewed.php index 7a0d67b56615..100ab7ab2d45 100644 --- a/app/Events/PurchaseOrder/PurchaseOrderWasViewed.php +++ b/app/Events/PurchaseOrder/PurchaseOrderWasViewed.php @@ -13,6 +13,7 @@ namespace App\Events\PurchaseOrder; use App\Models\Company; use App\Models\PurchaseOrder; +use App\Models\PurchaseOrderInvitation; use Illuminate\Queue\SerializesModels; /** @@ -25,7 +26,7 @@ class PurchaseOrderWasViewed /** * @var PurchaseOrder */ - public $purchase_order; + public $invitation; public $company; @@ -38,9 +39,9 @@ class PurchaseOrderWasViewed * @param Company $company * @param array $event_vars */ - public function __construct(PurchaseOrder $purchase_order, Company $company, array $event_vars) + public function __construct(PurchaseOrderInvitation $invitation, Company $company, array $event_vars) { - $this->purchase_order = $purchase_order; + $this->invitation = $invitation; $this->company = $company; $this->event_vars = $event_vars; } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 1a859942f4e7..f9bc844a31da 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -222,6 +222,9 @@ class Handler extends ExceptionHandler case 'user': $login = 'login'; break; + case 'vendor': + $login = 'vendor.catchall'; + break; default: $login = 'default'; break; diff --git a/app/Http/Controllers/Auth/VendorContactLoginController.php b/app/Http/Controllers/Auth/VendorContactLoginController.php new file mode 100644 index 000000000000..59fb20004e8f --- /dev/null +++ b/app/Http/Controllers/Auth/VendorContactLoginController.php @@ -0,0 +1,58 @@ +middleware('guest:vendor', ['except' => ['logout']]); + } + + public function catch() + { + $data = [ + + ]; + + return $this->render('purchase_orders.catch'); + } + + public function logout() + { + Auth::guard('vendor')->logout(); + request()->session()->invalidate(); + + return redirect('/vendors'); + } + + +} diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index e048291d7230..90bdc3429ff7 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -84,6 +84,7 @@ class BaseController extends Controller 'company.products.documents', 'company.payments.paymentables', 'company.payments.documents', + 'company.purchase_orders.documents', 'company.payment_terms.company', 'company.projects.documents', 'company.recurring_expenses', @@ -171,7 +172,12 @@ class BaseController extends Controller */ public function notFoundClient() { - abort(404, 'Page not found in client portal.'); + abort(404, 'Page not found in the client portal.'); + } + + public function notFoundVendor() + { + abort(404, 'Page not found in the vendor portal.'); } /** @@ -296,6 +302,13 @@ class BaseController extends Controller if(!$user->hasPermission('view_project')) $query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id); + }, + 'company.purchase_orders'=> function ($query) use ($updated_at, $user) { + $query->where('updated_at', '>=', $updated_at)->with('documents'); + + if(!$user->hasPermission('view_purchase_order')) + $query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id); + }, 'company.quotes'=> function ($query) use ($updated_at, $user) { $query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents'); @@ -533,6 +546,13 @@ class BaseController extends Controller if(!$user->hasPermission('view_project')) $query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id); + }, + 'company.purchase_orders'=> function ($query) use ($created_at, $user) { + $query->where('created_at', '>=', $created_at)->with('documents'); + + if(!$user->hasPermission('view_purchase_order')) + $query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id); + }, 'company.quotes'=> function ($query) use ($created_at, $user) { $query->where('created_at', '>=', $created_at)->with('invitations', 'documents'); diff --git a/app/Http/Controllers/ClientPortal/NinjaPlanController.php b/app/Http/Controllers/ClientPortal/NinjaPlanController.php index 79c419074209..b76e09ede7ec 100644 --- a/app/Http/Controllers/ClientPortal/NinjaPlanController.php +++ b/app/Http/Controllers/ClientPortal/NinjaPlanController.php @@ -154,6 +154,7 @@ class NinjaPlanController extends Controller $recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill); $recurring_invoice->due_date_days = 'terms'; $recurring_invoice->next_send_date = now()->addDays(14)->format('Y-m-d'); + $recurring_invoice->next_send_date_client = now()->addDays(14)->format('Y-m-d'); $recurring_invoice->save(); $r = $recurring_invoice->calc()->getRecurringInvoice(); diff --git a/app/Http/Controllers/SelfUpdateController.php b/app/Http/Controllers/SelfUpdateController.php index 4f14a006b04b..06105ab32e20 100644 --- a/app/Http/Controllers/SelfUpdateController.php +++ b/app/Http/Controllers/SelfUpdateController.php @@ -135,6 +135,9 @@ class SelfUpdateController extends BaseController nlog("Extracting zip"); + //clean up old snappdf installations + $this->cleanOldSnapChromeBinaries(); + // try{ // $s = new Snappdf; // $s->getChromiumPath(); @@ -188,6 +191,21 @@ class SelfUpdateController extends BaseController return response()->json(['message' => 'Update completed'], 200); + } + + private function cleanOldSnapChromeBinaries() + { + $current_revision = base_path('vendor/beganovich/snappdf/versions/revision.txt'); + + $directoryIterator = new \RecursiveDirectoryIterator(base_path('vendor/beganovich/snappdf/versions'), \RecursiveDirectoryIterator::SKIP_DOTS); + + foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) { + + unlink($file->getPathName()); + + } + + } private function postHookUpdate() diff --git a/app/Http/Controllers/SubdomainController.php b/app/Http/Controllers/SubdomainController.php index 6ad54f007535..63b4812e3ba9 100644 --- a/app/Http/Controllers/SubdomainController.php +++ b/app/Http/Controllers/SubdomainController.php @@ -35,6 +35,7 @@ class SubdomainController extends BaseController 'html', 'lb', 'shopify', + 'beta', ]; public function __construct() diff --git a/app/Http/Controllers/VendorPortal/InvitationController.php b/app/Http/Controllers/VendorPortal/InvitationController.php new file mode 100644 index 000000000000..ba6abc9c25b6 --- /dev/null +++ b/app/Http/Controllers/VendorPortal/InvitationController.php @@ -0,0 +1,145 @@ +whereHas('purchase_order', function ($query) { + $query->where('is_deleted',0); + }) + ->with('contact.vendor') + ->first(); + + if(!$invitation) + return abort(404,'The resource is no longer available.'); + + if($invitation->contact->trashed()) + $invitation->contact->restore(); + + $vendor_contact = $invitation->contact; + $entity = 'purchase_order'; + + if(empty($vendor_contact->email)) + $vendor_contact->email = Str::random(15) . "@example.com"; $vendor_contact->save(); + + if (request()->has('vendor_hash') && request()->input('vendor_hash') == $invitation->contact->vendor->vendor_hash) { + request()->session()->invalidate(); + auth()->guard('vendor')->loginUsingId($vendor_contact->id, true); + + } else { + nlog("else - default - login contact"); + request()->session()->invalidate(); + auth()->guard('vendor')->loginUsingId($vendor_contact->id, true); + } + + if (auth()->guard('vendor')->user() && ! request()->has('silent') && ! $invitation->viewed_date) { + + if(!session()->get('is_silent')){ + + $invitation->markViewed(); + event(new InvitationWasViewed($invitation->purchase_order, $invitation, $invitation->company, Ninja::eventVars())); + } + + } + else{ + + return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id), 'silent' => session()->get('is_silent')]); + + } + + return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id)]); + + + } + + + + // public function routerForDownload(string $entity, string $invitation_key) + // { + + // set_time_limit(45); + + // if(Ninja::isHosted()) + // return $this->returnRawPdf($entity, $invitation_key); + + // return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf'); + // } + + // private function returnRawPdf(string $entity, string $invitation_key) + // { + + // if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice'])) + // return response()->json(['message' => 'Invalid resource request']); + + // $key = $entity.'_id'; + + // $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; + + // $invitation = $entity_obj::where('key', $invitation_key) + // ->with('contact.client') + // ->firstOrFail(); + + // if(!$invitation) + // return response()->json(["message" => "no record found"], 400); + + // $file_name = $invitation->purchase_order->numberFormatter().'.pdf'; + + // $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db); + + // $headers = ['Content-Type' => 'application/pdf']; + + // if(request()->input('inline') == 'true') + // $headers = array_merge($headers, ['Content-Disposition' => 'inline']); + + // return response()->streamDownload(function () use($file) { + // echo $file; + // }, $file_name, $headers); + + // } + + + +} diff --git a/app/Http/Controllers/VendorPortal/PurchaseOrderController.php b/app/Http/Controllers/VendorPortal/PurchaseOrderController.php new file mode 100644 index 000000000000..8b0b98e076ab --- /dev/null +++ b/app/Http/Controllers/VendorPortal/PurchaseOrderController.php @@ -0,0 +1,229 @@ +render('purchase_orders.index', ['company' => auth()->user()->company, 'settings' => auth()->user()->company->settings, 'sidebar' => $this->sidebarMenu()]); + } + + /** + * Show specific invoice. + * + * @param ShowInvoiceRequest $request + * @param Invoice $invoice + * + * @return Factory|View + */ + public function show(ShowPurchaseOrderRequest $request, PurchaseOrder $purchase_order) + { + set_time_limit(0); + + $invitation = $purchase_order->invitations()->where('vendor_contact_id', auth()->guard('vendor')->user()->id)->first(); + + if ($invitation && auth()->guard('vendor') && !session()->get('is_silent') && ! $invitation->viewed_date) { + + $invitation->markViewed(); + + event(new InvitationWasViewed($purchase_order, $invitation, $purchase_order->company, Ninja::eventVars())); + event(new PurchaseOrderWasViewed($invitation, $invitation->company, Ninja::eventVars())); + + } + + + $data = [ + 'purchase_order' => $purchase_order, + 'key' => $invitation ? $invitation->key : false, + 'settings' => $purchase_order->company->settings, + 'sidebar' => $this->sidebarMenu(), + 'company' => $purchase_order->company + ]; + + if ($request->query('mode') === 'fullscreen') { + return render('purchase_orders.show-fullscreen', $data); + } + + return $this->render('purchase_orders.show', $data); + } + + + + private function sidebarMenu() :array + { + $enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules; + $data = []; + + // TODO: Enable dashboard once it's completed. + // $this->settings->enable_client_portal_dashboard + // $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity']; + + if (self::MODULE_PURCHASE_ORDERS & $enabled_modules) { + $data[] = ['title' => ctrans('texts.purchase_orders'), 'url' => 'vendor.purchase_orders.index', 'icon' => 'file-text']; + } + + // $data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download']; + + return $data; + } + + public function bulk(ProcessPurchaseOrdersInBulkRequest $request) + { + + $transformed_ids = $this->transformKeys($request->purchase_orders); + + if ($request->input('action') == 'download') { + return $this->downloadInvoices((array) $transformed_ids); + } + elseif ($request->input('action') == 'accept'){ + return $this->acceptPurchaseOrder($request->all()); + } + + return redirect() + ->back() + ->with('message', ctrans('texts.no_action_provided')); + } + + public function acceptPurchaseOrder($data) + { + $purchase_orders = PurchaseOrder::query() + ->whereIn('id', $this->transformKeys($data['purchase_orders'])) + ->where('company_id', auth()->guard('vendor')->user()->vendor->company_id) + ->whereIn('status_id', [PurchaseOrder::STATUS_DRAFT, PurchaseOrder::STATUS_SENT]) + ->cursor()->each(function ($purchase_order){ + + $purchase_order->service() + ->markSent() + ->applyNumber() + ->setStatus(PurchaseOrder::STATUS_ACCEPTED) + ->save(); + + if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) { + InjectSignature::dispatch($purchase_order, request()->signature); + } + + event(new PurchaseOrderWasAccepted($purchase_order, auth()->guard('vendor')->user(), $purchase_order->company, Ninja::eventVars())); + + }); + + if(count($data['purchase_orders']) == 1){ + + $purchase_order = PurchaseOrder::whereIn('id', $this->transformKeys($data['purchase_orders']))->first(); + + return redirect()->route('vendor.purchase_order.show', ['purchase_order' => $purchase_order->hashed_id]); + + } + else + return redirect()->route('vendor.purchase_orders.index'); + + + } + + public function downloadInvoices($ids) + { + + $purchase_orders = PurchaseOrder::whereIn('id', $ids) + ->where('vendor_id', auth()->guard('vendor')->user()->vendor_id) + ->withTrashed() + ->get(); + + if(count($purchase_orders) == 0) + return back()->with(['message' => ctrans('texts.no_items_selected')]); + + + if(count($purchase_orders) == 1){ + + $purchase_order = $purchase_orders->first(); + + $file = $purchase_order->service()->getPurchaseOrderPdf(auth()->guard('vendor')->user()); + + return response()->streamDownload(function () use($file) { + echo Storage::get($file); + }, basename($file), ['Content-Type' => 'application/pdf']); + } + + return $this->buildZip($purchase_orders); + } + + + private function buildZip($purchase_orders) + { + // create new archive + $zipFile = new \PhpZip\ZipFile(); + try{ + + foreach ($purchase_orders as $purchase_order) { + + #add it to the zip + $zipFile->addFromString(basename($purchase_order->pdf_file_path()), file_get_contents($purchase_order->pdf_file_path(null, 'url', true))); + + } + + $filename = date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.purchase_orders')).'.zip'; + $filepath = sys_get_temp_dir() . '/' . $filename; + + $zipFile->saveAsFile($filepath) // save the archive to a file + ->close(); // close archive + + return response()->download($filepath, $filename)->deleteFileAfterSend(true); + + } + catch(\PhpZip\Exception\ZipException $e){ + // handle exception + } + finally{ + $zipFile->close(); + } + } +} diff --git a/app/Http/Controllers/VendorPortal/VendorContactController.php b/app/Http/Controllers/VendorPortal/VendorContactController.php new file mode 100644 index 000000000000..7aa93db123e0 --- /dev/null +++ b/app/Http/Controllers/VendorPortal/VendorContactController.php @@ -0,0 +1,81 @@ +render('vendor_profile.edit', [ + 'contact' => $vendor_contact, + 'vendor' => $vendor_contact->vendor, + 'settings' => $vendor_contact->vendor->company->settings, + 'company' => $vendor_contact->vendor->company, + 'sidebar' => $this->sidebarMenu(), + 'countries' => TranslationHelper::getCountries() + ]); + } + + public function update(VendorContact $vendor_contact) + { + $vendor_contact->fill(request()->all()); + $vendor_contact->vendor->fill(request()->all()); + $vendor_contact->push(); + + return back()->withSuccess(ctrans('texts.profile_updated_successfully')); + + } + + private function sidebarMenu() :array + { + $enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules; + $data = []; + + // TODO: Enable dashboard once it's completed. + // $this->settings->enable_client_portal_dashboard + // $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity']; + + if (self::MODULE_PURCHASE_ORDERS & $enabled_modules) { + $data[] = ['title' => ctrans('texts.purchase_orders'), 'url' => 'vendor.purchase_orders.index', 'icon' => 'file-text']; + } + + // $data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download']; + + return $data; + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/WePayController.php b/app/Http/Controllers/WePayController.php index feed2504488b..f615e3b7f36b 100644 --- a/app/Http/Controllers/WePayController.php +++ b/app/Http/Controllers/WePayController.php @@ -29,7 +29,7 @@ class WePayController extends BaseController */ public function signup(string $token) { - return render('gateways.wepay.signup.finished'); + // return render('gateways.wepay.signup.finished'); $hash = Cache::get($token); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 85f1ecc66238..9557ade3a718 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -42,6 +42,7 @@ use App\Http\Middleware\TrimStrings; use App\Http\Middleware\TrustProxies; use App\Http\Middleware\UrlSetDb; use App\Http\Middleware\UserVerified; +use App\Http\Middleware\VendorLocale; use App\Http\Middleware\VerifyCsrfToken; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\Middleware\Authorize; @@ -158,6 +159,7 @@ class Kernel extends HttpKernel 'api_db' => SetDb::class, 'company_key_db' => SetDbByCompanyKey::class, 'locale' => Locale::class, + 'vendor_locale' => VendorLocale::class, 'contact_register' => ContactRegister::class, 'shop_token_auth' => ShopTokenAuth::class, 'phantom_secret' => PhantomSecret::class, diff --git a/app/Http/Livewire/PurchaseOrdersTable.php b/app/Http/Livewire/PurchaseOrdersTable.php new file mode 100644 index 000000000000..13b057297669 --- /dev/null +++ b/app/Http/Livewire/PurchaseOrdersTable.php @@ -0,0 +1,76 @@ +company->db); + + $this->sort_asc = false; + + $this->sort_field = 'date'; + } + + public function render() + { + $local_status = []; + + $query = PurchaseOrder::query() + ->with('vendor.contacts') + ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') + ->whereIn('status_id', [PurchaseOrder::STATUS_SENT, PurchaseOrder::STATUS_ACCEPTED]) + ->where('company_id', $this->company->id) + ->where('is_deleted', false); + + if (in_array('sent', $this->status)) { + $local_status[] = PurchaseOrder::STATUS_SENT; + } + + if (in_array('accepted', $this->status)) { + $local_status[] = PurchaseOrder::STATUS_ACCEPTED; + } + + if (count($local_status) > 0) { + $query = $query->whereIn('status_id', array_unique($local_status)); + } + + $query = $query + ->where('vendor_id', auth()->guard('vendor')->user()->vendor_id) + // ->where('status_id', '<>', Invoice::STATUS_DRAFT) + // ->where('status_id', '<>', Invoice::STATUS_CANCELLED) + ->withTrashed() + ->paginate($this->per_page); + + return render('components.livewire.purchase-orders-table', [ + 'purchase_orders' => $query + ]); + } +} diff --git a/app/Http/Middleware/SetInviteDb.php b/app/Http/Middleware/SetInviteDb.php index f6fd258a9e5e..b13782900ca0 100644 --- a/app/Http/Middleware/SetInviteDb.php +++ b/app/Http/Middleware/SetInviteDb.php @@ -46,7 +46,7 @@ class SetInviteDb if($entity == "pay") $entity = "invoice"; - if(!in_array($entity, ['invoice','quote','credit','recurring_invoice'])) + if(!in_array($entity, ['invoice','quote','credit','recurring_invoice','purchase_order'])) abort(404,'I could not find this resource.'); /* Try and determine the DB from the invitation key STRING*/ diff --git a/app/Http/Middleware/VendorLocale.php b/app/Http/Middleware/VendorLocale.php new file mode 100644 index 000000000000..490973f00ddb --- /dev/null +++ b/app/Http/Middleware/VendorLocale.php @@ -0,0 +1,57 @@ +guard('contact')->check()) { + auth()->guard('contact')->logout(); + $request->session()->invalidate(); + } + + + /*LOCALE SET */ + if ($request->has('lang')) { + $locale = $request->input('lang'); + App::setLocale($locale); + } elseif (auth()->guard('vendor')->user()) { + App::setLocale(auth()->guard('vendor')->user()->company->locale()); + } elseif (auth()->user()) { + + try{ + App::setLocale(auth()->user()->company()->getLocale()); + } + catch(\Exception $e){ + } + + } else { + App::setLocale(config('ninja.i18n.locale')); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/Gateways/Checkout3ds/Checkout3dsRequest.php b/app/Http/Requests/Gateways/Checkout3ds/Checkout3dsRequest.php index c0d5e16c547f..3588d8ca953a 100644 --- a/app/Http/Requests/Gateways/Checkout3ds/Checkout3dsRequest.php +++ b/app/Http/Requests/Gateways/Checkout3ds/Checkout3dsRequest.php @@ -39,21 +39,28 @@ class Checkout3dsRequest extends FormRequest public function getCompany() { MultiDB::findAndSetDbByCompanyKey($this->company_key); + return Company::where('company_key', $this->company_key)->first(); } public function getCompanyGateway() { + MultiDB::findAndSetDbByCompanyKey($this->company_key); + return CompanyGateway::find($this->decodePrimaryKey($this->company_gateway_id)); } public function getPaymentHash() { + MultiDB::findAndSetDbByCompanyKey($this->company_key); + return PaymentHash::where('hash', $this->hash)->first(); } public function getClient() { - return Client::find($this->getPaymentHash()->data->client_id); + MultiDB::findAndSetDbByCompanyKey($this->company_key); + + return Client::withTrashed()->find($this->getPaymentHash()->data->client_id); } } diff --git a/app/Http/Requests/VendorPortal/PurchaseOrders/ProcessPurchaseOrdersInBulkRequest.php b/app/Http/Requests/VendorPortal/PurchaseOrders/ProcessPurchaseOrdersInBulkRequest.php new file mode 100644 index 000000000000..88244f1edc6e --- /dev/null +++ b/app/Http/Requests/VendorPortal/PurchaseOrders/ProcessPurchaseOrdersInBulkRequest.php @@ -0,0 +1,31 @@ +guard('vendor')->user()->vendor->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS; + } + + public function rules() + { + return [ + 'purchase_orders' => ['array'], + ]; + } +} diff --git a/app/Http/Requests/VendorPortal/PurchaseOrders/ShowPurchaseOrderRequest.php b/app/Http/Requests/VendorPortal/PurchaseOrders/ShowPurchaseOrderRequest.php new file mode 100644 index 000000000000..03b074d04fb8 --- /dev/null +++ b/app/Http/Requests/VendorPortal/PurchaseOrders/ShowPurchaseOrderRequest.php @@ -0,0 +1,29 @@ +guard('vendor')->user()->vendor_id === (int)$this->purchase_order->vendor_id + && auth()->guard('vendor')->user()->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS; + } +} diff --git a/app/Http/Requests/VendorPortal/PurchaseOrders/ShowPurchaseOrdersRequest.php b/app/Http/Requests/VendorPortal/PurchaseOrders/ShowPurchaseOrdersRequest.php new file mode 100644 index 000000000000..c15a2bc3e04d --- /dev/null +++ b/app/Http/Requests/VendorPortal/PurchaseOrders/ShowPurchaseOrdersRequest.php @@ -0,0 +1,29 @@ +guard('vendor')->user()->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS; + + } +} diff --git a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php index 2b601a8d1fd5..fde78e220368 100644 --- a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php +++ b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php @@ -70,7 +70,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue * * @param $invitation */ - public function __construct($invitation, $disk = 'public') + public function __construct($invitation, $disk = null) { $this->invitation = $invitation; $this->company = $invitation->company; @@ -83,7 +83,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue $this->vendor = $invitation->contact->vendor; $this->vendor->load('company'); - $this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk; + $this->disk = $disk ?? config('filesystems.default'); } diff --git a/app/Listeners/Misc/InvitationViewedListener.php b/app/Listeners/Misc/InvitationViewedListener.php index 773cc50be5d8..70275dc4498a 100644 --- a/app/Listeners/Misc/InvitationViewedListener.php +++ b/app/Listeners/Misc/InvitationViewedListener.php @@ -49,7 +49,9 @@ class InvitationViewedListener implements ShouldQueue if($entity_name == 'recurringInvoice') return; - + elseif($entity_name == 'purchaseOrder') + $entity_name = 'purchase_order'; + $nmo = new NinjaMailerObject; $nmo->mailable = new NinjaMailer( (new EntityViewedObject($invitation, $entity_name))->build() ); $nmo->company = $invitation->company; @@ -60,6 +62,8 @@ class InvitationViewedListener implements ShouldQueue $entity_viewed = "{$entity_name}_viewed"; $entity_viewed_all = "{$entity_name}_viewed_all"; + + $methods = $this->findUserNotificationTypes($invitation, $company_user, $entity_name, ['all_notifications', $entity_viewed, $entity_viewed_all]); if (($key = array_search('mail', $methods)) !== false) { diff --git a/app/Listeners/PurchaseOrder/PurchaseOrderAcceptedActivity.php b/app/Listeners/PurchaseOrder/PurchaseOrderAcceptedActivity.php new file mode 100644 index 000000000000..6baba48e0914 --- /dev/null +++ b/app/Listeners/PurchaseOrder/PurchaseOrderAcceptedActivity.php @@ -0,0 +1,61 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $fields = new stdClass; + + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->purchase_order->user_id; + + $event->purchase_order->service()->markSent()->save(); + + $fields->user_id = $user_id; + $fields->company_id = $event->purchase_order->company_id; + $fields->activity_type_id = Activity::ACCEPT_PURCHASE_ORDER; + $fields->vendor_id = $event->purchase_order->vendor_id; + $fields->vendor_contact_id = $event->contact->id; + $fields->purchase_order_id = $event->purchase_order->id; + + $this->activity_repo->save($fields, $event->purchase_order, $event->event_vars); + } +} diff --git a/app/Mail/Admin/EntityViewedObject.php b/app/Mail/Admin/EntityViewedObject.php index fa2f7b9d24a9..85e4d827c34c 100644 --- a/app/Mail/Admin/EntityViewedObject.php +++ b/app/Mail/Admin/EntityViewedObject.php @@ -65,7 +65,12 @@ class EntityViewedObject private function getAmount() { - return Number::formatMoney($this->entity->amount, $this->entity->client); + if($this->entity->client) + $currency_entity = $this->entity->client; + else + $currency_entity = $this->company; + + return Number::formatMoney($this->entity->amount, $currency_entity); } private function getSubject() @@ -82,7 +87,10 @@ class EntityViewedObject private function getData() { - $settings = $this->entity->client->getMergedSettings(); + if($this->entity->client) + $settings = $this->entity->client->getMergedSettings(); + else + $settings = $this->company->settings; $data = [ 'title' => $this->getSubject(), diff --git a/app/Models/Account.php b/app/Models/Account.php index 4f9f9a333e0d..b2a94fb95dff 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -88,6 +88,7 @@ class Account extends BaseModel const FEATURE_TASKS = 'tasks'; const FEATURE_EXPENSES = 'expenses'; const FEATURE_QUOTES = 'quotes'; + const FEATURE_PURCHASE_ORDERS = 'purchase_orders'; const FEATURE_CUSTOMIZE_INVOICE_DESIGN = 'custom_designs'; const FEATURE_DIFFERENT_DESIGNS = 'different_designs'; const FEATURE_EMAIL_TEMPLATES_REMINDERS = 'template_reminders'; @@ -163,6 +164,7 @@ class Account extends BaseModel case self::FEATURE_TASKS: case self::FEATURE_EXPENSES: case self::FEATURE_QUOTES: + case self::FEATURE_PURCHASE_ORDERS: return true; case self::FEATURE_CUSTOMIZE_INVOICE_DESIGN: diff --git a/app/Models/Activity.php b/app/Models/Activity.php index f5a8ce9eb297..570fc16eda91 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -116,7 +116,8 @@ class Activity extends StaticModel const RESTORE_PURCHASE_ORDER = 134; const EMAIL_PURCHASE_ORDER = 135; const VIEW_PURCHASE_ORDER = 136; - + const ACCEPT_PURCHASE_ORDER = 137; + protected $casts = [ 'is_system' => 'boolean', 'updated_at' => 'timestamp', diff --git a/app/Models/Company.php b/app/Models/Company.php index 4c02942c854e..10977900fe42 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -14,6 +14,7 @@ namespace App\Models; use App\DataMapper\CompanySettings; use App\Models\Language; use App\Models\Presenters\CompanyPresenter; +use App\Models\PurchaseOrder; use App\Models\User; use App\Services\Notification\NotificationService; use App\Utils\Ninja; @@ -192,6 +193,11 @@ class Company extends BaseModel return $this->hasMany(Subscription::class)->withTrashed(); } + public function purchase_orders() + { + return $this->hasMany(PurchaseOrder::class)->withTrashed(); + } + public function task_statuses() { return $this->hasMany(TaskStatus::class)->withTrashed(); diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 67e8a2d2a63e..70419412e960 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -18,6 +18,7 @@ use App\Jobs\Entity\CreateEntityPdf; use App\Jobs\Vendor\CreatePurchaseOrderPdf; use App\Services\PurchaseOrder\PurchaseOrderService; use App\Utils\Ninja; +use App\Utils\Traits\MakesDates; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; @@ -26,6 +27,7 @@ class PurchaseOrder extends BaseModel { use Filterable; use SoftDeletes; + use MakesDates; protected $fillable = [ 'number', @@ -99,8 +101,51 @@ class PurchaseOrder extends BaseModel const STATUS_DRAFT = 1; const STATUS_SENT = 2; - const STATUS_PARTIAL = 3; - const STATUS_APPLIED = 4; + const STATUS_ACCEPTED = 3; + const STATUS_CANCELLED = 4; + + public static function stringStatus(int $status) + { + switch ($status) { + case self::STATUS_DRAFT: + return ctrans('texts.draft'); + break; + case self::STATUS_SENT: + return ctrans('texts.sent'); + break; + case self::STATUS_ACCEPTED: + return ctrans('texts.accepted'); + break; + case self::STATUS_CANCELLED: + return ctrans('texts.cancelled'); + break; + // code... + break; + } + } + + + public static function badgeForStatus(int $status) + { + switch ($status) { + case self::STATUS_DRAFT: + return '
'.ctrans('texts.draft').'
'; + break; + case self::STATUS_SENT: + return '
'.ctrans('texts.sent').'
'; + break; + case self::STATUS_ACCEPTED: + return '
'.ctrans('texts.accepted').'
'; + break; + case self::STATUS_CANCELLED: + return '
'.ctrans('texts.cancelled').'
'; + break; + default: + // code... + break; + } + } + public function assigned_user() { diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php index d4a4bd92f91a..d0661f943a95 100644 --- a/app/Models/VendorContact.php +++ b/app/Models/VendorContact.php @@ -73,6 +73,24 @@ class VendorContact extends Authenticatable implements HasLocalePreference 'vendor_id', ]; + public function avatar() + { + if ($this->avatar) { + return $this->avatar; + } + + return asset('images/svg/user.svg'); + } + + public function setAvatarAttribute($value) + { + if (! filter_var($value, FILTER_VALIDATE_URL) && $value) { + $this->attributes['avatar'] = url('/').$value; + } else { + $this->attributes['avatar'] = $value; + } + } + public function getEntityType() { return self::class; diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 2a2b58ccc5e2..f6d76512d1cb 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -407,12 +407,7 @@ class BaseDriver extends AbstractPaymentDriver $this->unWindGatewayFees($this->payment_hash); } - if ($e instanceof CheckoutHttpException) { - $error = $e->getBody(); - } else if ($e instanceof Exception) { - $error = $e->getMessage(); - } else - $error = $e->getMessage(); + $error = $e->getMessage(); if(!$this->payment_hash) throw new PaymentFailed($error, $e->getCode()); diff --git a/app/PaymentDrivers/CheckoutCom/CreditCard.php b/app/PaymentDrivers/CheckoutCom/CreditCard.php index 419142eb0b95..033f5865c93e 100644 --- a/app/PaymentDrivers/CheckoutCom/CreditCard.php +++ b/app/PaymentDrivers/CheckoutCom/CreditCard.php @@ -14,18 +14,24 @@ namespace App\PaymentDrivers\CheckoutCom; use App\Exceptions\PaymentFailed; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; -use Illuminate\Http\Request; use App\Models\ClientGatewayToken; use App\Models\GatewayType; +use App\Models\Payment; use App\PaymentDrivers\CheckoutComPaymentDriver; use App\PaymentDrivers\Common\MethodInterface; use App\Utils\Traits\MakesHash; +use Checkout\CheckoutApiException; +use Checkout\CheckoutArgumentException; +use Checkout\CheckoutAuthorizationException; use Checkout\Library\Exceptions\CheckoutHttpException; use Checkout\Models\Payments\IdSource; -use Checkout\Models\Payments\Payment; -use Checkout\Models\Payments\TokenSource; +use Checkout\Payments\Four\Request\Source\RequestTokenSource; +use Checkout\Payments\Source\RequestTokenSource as SourceRequestTokenSource; use Illuminate\Contracts\View\Factory; +use Illuminate\Http\Request; use Illuminate\View\View; +use Checkout\Payments\PaymentRequest as PaymentsPaymentRequest; +use Checkout\Payments\Four\Request\PaymentRequest; class CreditCard implements MethodInterface { @@ -57,6 +63,31 @@ class CreditCard implements MethodInterface return render('gateways.checkout.credit_card.authorize', $data); } + public function bootRequest($token) + { + + if($this->checkout->is_four_api){ + + $token_source = new RequestTokenSource(); + $token_source->token = $token; + $request = new PaymentRequest(); + $request->source = $token_source; + + + } + else { + + $token_source = new SourceRequestTokenSource(); + $token_source->token = $token; + $request = new PaymentsPaymentRequest(); + $request->source = $token_source; + + } + + return $request; + + } + /** * Handle authorization for credit card. * @@ -67,41 +98,54 @@ class CreditCard implements MethodInterface { $gateway_response = \json_decode($request->gateway_response); - $method = new TokenSource( - $gateway_response->token - ); + $customerRequest = $this->checkout->getCustomer(); + $request = $this->bootRequest($gateway_response->token); + $request->capture = false; + $request->reference = '$1 payment for authorization.'; + $request->amount = 100; + $request->currency = $this->checkout->client->getCurrencyCode(); + $request->customer = $customerRequest; - $payment = new Payment($method, 'USD'); - $payment->amount = 100; // $1 - $payment->reference = '$1 payment for authorization.'; - $payment->capture = false; try { - $response = $this->checkout->gateway->payments()->request($payment); - if ($response->approved && $response->status === 'Authorized') { + $response = $this->checkout->gateway->getPaymentsClient()->requestPayment($request); + + if ($response['approved'] && $response['status'] === 'Authorized') { $payment_meta = new \stdClass; - $payment_meta->exp_month = (string) $response->source['expiry_month']; - $payment_meta->exp_year = (string) $response->source['expiry_year']; - $payment_meta->brand = (string) $response->source['scheme']; - $payment_meta->last4 = (string) $response->source['last4']; + $payment_meta->exp_month = (string) $response['source']['expiry_month']; + $payment_meta->exp_year = (string) $response['source']['expiry_year']; + $payment_meta->brand = (string) $response['source']['scheme']; + $payment_meta->last4 = (string) $response['source']['last4']; $payment_meta->type = (int) GatewayType::CREDIT_CARD; $data = [ 'payment_meta' => $payment_meta, - 'token' => $response->source['id'], - 'payment_method_id' => GatewayType::CREDIT_CARD, + 'token' => $response['source']['id'], + 'payment_method_id' => GatewayType::CREDIT_CARD, ]; - $payment_method = $this->checkout->storeGatewayToken($data); + $payment_method = $this->checkout->storeGatewayToken($data,['gateway_customer_reference' => $customerRequest['id']]); return redirect()->route('client.payment_methods.show', $payment_method->hashed_id); } - } catch (CheckoutHttpException $exception) { - throw new PaymentFailed( - $exception->getMessage() - ); + + + } catch (CheckoutApiException $e) { + // API error + $request_id = $e->request_id; + $http_status_code = $e->http_status_code; + $error_details = $e->error_details; + + throw new PaymentFailed($e->getMessage()); + } catch (CheckoutArgumentException $e) { + // Bad arguments + throw new PaymentFailed($e->getMessage()); + } catch (CheckoutAuthorizationException $e) { + // Bad Invalid authorization + throw new PaymentFailed($e->getMessage()); } + } public function paymentView($data) @@ -145,89 +189,102 @@ class CreditCard implements MethodInterface { $cgt = ClientGatewayToken::query() ->where('id', $this->decodePrimaryKey($request->input('token'))) - ->where('company_id', auth()->guard('contact')->user()->client->company->id) + ->where('company_id', auth()->guard('contact')->user()->client->company_id) ->first(); if (!$cgt) { throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401); } - $method = new IdSource($cgt->token); + $paymentRequest = $this->checkout->bootTokenRequest($cgt->token); - return $this->completePayment($method, $request); + return $this->completePayment($paymentRequest, $request); } private function attemptPaymentUsingCreditCard(PaymentResponseRequest $request) { $checkout_response = $this->checkout->payment_hash->data->server_response; - $method = new TokenSource( - $checkout_response->token - ); + $paymentRequest = $this->bootRequest($checkout_response->token); - return $this->completePayment($method, $request); + return $this->completePayment($paymentRequest, $request); } - private function completePayment($method, PaymentResponseRequest $request) + private function completePayment($paymentRequest, PaymentResponseRequest $request) { - $payment = new Payment($method, $this->checkout->payment_hash->data->currency); - $payment->amount = $this->checkout->payment_hash->data->value; - $payment->reference = $this->checkout->getDescription(); - $payment->customer = [ - 'name' => $this->checkout->client->present()->name() , - 'email' => $this->checkout->client->present()->email(), - ]; + $paymentRequest->amount = $this->checkout->payment_hash->data->value; + $paymentRequest->reference = $this->checkout->getDescription(); + $paymentRequest->customer = $this->checkout->getCustomer(); + $paymentRequest->metadata = ['udf1' => "Invoice Ninja"]; + $paymentRequest->currency = $this->checkout->client->getCurrencyCode(); - $payment->metadata = [ - 'udf1' => "Invoice Ninja", - ]; - - $this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, ['checkout_payment_ref' => $payment]); + $this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, ['checkout_payment_ref' => $paymentRequest]); $this->checkout->payment_hash->save(); if ($this->checkout->client->currency()->code == 'EUR' || $this->checkout->company_gateway->getConfigField('threeds')) { - $payment->{'3ds'} = ['enabled' => true]; - $payment->{'success_url'} = route('checkout.3ds_redirect', [ + $paymentRequest->{'3ds'} = ['enabled' => true]; + + $paymentRequest->{'success_url'} = route('checkout.3ds_redirect', [ 'company_key' => $this->checkout->client->company->company_key, 'company_gateway_id' => $this->checkout->company_gateway->hashed_id, 'hash' => $this->checkout->payment_hash->hash, ]); - $payment->{'failure_url'} = route('checkout.3ds_redirect', [ + $paymentRequest->{'failure_url'} = route('checkout.3ds_redirect', [ 'company_key' => $this->checkout->client->company->company_key, 'company_gateway_id' => $this->checkout->company_gateway->hashed_id, 'hash' => $this->checkout->payment_hash->hash, ]); + } try { - $response = $this->checkout->gateway->payments()->request($payment); + // $response = $this->checkout->gateway->payments()->request($payment); - if ($response->status == 'Authorized') { + $response = $this->checkout->gateway->getPaymentsClient()->requestPayment($paymentRequest); + + if ($response['status'] == 'Authorized') { return $this->processSuccessfulPayment($response); } - if ($response->status == 'Pending') { + if ($response['status'] == 'Pending') { $this->checkout->confirmGatewayFee(); return $this->processPendingPayment($response); } - if ($response->status == 'Declined') { + if ($response['status'] == 'Declined') { + $this->checkout->unWindGatewayFees($this->checkout->payment_hash); - // $this->checkout->sendFailureMail($response->response_summary); - - //@todo - this will double up the checkout . com failed mails - // $this->checkout->clientPaymentFailureMailer($response->status); - return $this->processUnsuccessfulPayment($response); } - } catch (CheckoutHttpException $e) { + } + catch (CheckoutApiException $e) { + // API error + $request_id = $e->request_id; + $http_status_code = $e->http_status_code; + $error_details = $e->error_details; + + $this->checkout->unWindGatewayFees($this->checkout->payment_hash); return $this->checkout->processInternallyFailedPayment($this->checkout, $e); + + } catch (CheckoutArgumentException $e) { + // Bad arguments + + $this->checkout->unWindGatewayFees($this->checkout->payment_hash); + return $this->checkout->processInternallyFailedPayment($this->checkout, $e); + + } catch (CheckoutAuthorizationException $e) { + // Bad Invalid authorization + + $this->checkout->unWindGatewayFees($this->checkout->payment_hash); + return $this->checkout->processInternallyFailedPayment($this->checkout, $e); + } + } } diff --git a/app/PaymentDrivers/CheckoutCom/Utilities.php b/app/PaymentDrivers/CheckoutCom/Utilities.php index a069fea44d91..014046963ba4 100644 --- a/app/PaymentDrivers/CheckoutCom/Utilities.php +++ b/app/PaymentDrivers/CheckoutCom/Utilities.php @@ -54,17 +54,18 @@ trait Utilities return round($amount * 100); } - private function processSuccessfulPayment(Payment $_payment) + private function processSuccessfulPayment($_payment) { + if ($this->getParent()->payment_hash->data->store_card) { $this->storeLocalPaymentMethod($_payment); } $data = [ - 'payment_method' => $_payment->source['id'], + 'payment_method' => $_payment['source']['id'], 'payment_type' => 12, 'amount' => $this->getParent()->payment_hash->data->raw_value, - 'transaction_reference' => $_payment->id, + 'transaction_reference' => $_payment['id'], 'gateway_type_id' => GatewayType::CREDIT_CARD, ]; @@ -82,15 +83,15 @@ trait Utilities return redirect()->route('client.payments.show', ['payment' => $this->getParent()->encodePrimaryKey($payment->id)]); } - public function processUnsuccessfulPayment(Payment $_payment, $throw_exception = true) + public function processUnsuccessfulPayment($_payment, $throw_exception = true) { $error_message = ''; - if(property_exists($_payment, 'server_response')) - $error_message = $_payment->response_summary; - elseif(property_exists($_payment, 'status')) - $error_message = $_payment->status; + if(array_key_exists('response_summary',$_payment)) + $error_message = $_payment['response_summary']; + elseif(array_key_exists('status',$_payment)) + $error_message = $_payment['status']; $this->getParent()->sendFailureMail($error_message); @@ -110,36 +111,37 @@ trait Utilities if ($throw_exception) { - throw new PaymentFailed($_payment->status . " " . $error_message, $_payment->http_code); + throw new PaymentFailed($_payment['status'] . " " . $error_message, 500); } } - private function processPendingPayment(Payment $_payment) + private function processPendingPayment($_payment) { + try { - return redirect($_payment->_links['redirect']['href']); + return redirect($_payment['_links']['redirect']['href']); } catch (Exception $e) { - return $this->processInternallyFailedPayment($this->getParent(), $e); + return $this->getParent()->processInternallyFailedPayment($this->getParent(), $e); } } - private function storeLocalPaymentMethod(Payment $response) + private function storeLocalPaymentMethod($response) { try { $payment_meta = new stdClass; - $payment_meta->exp_month = (string) $response->source['expiry_month']; - $payment_meta->exp_year = (string) $response->source['expiry_year']; - $payment_meta->brand = (string) $response->source['scheme']; - $payment_meta->last4 = (string) $response->source['last4']; + $payment_meta->exp_month = (string) $response['source']['expiry_month']; + $payment_meta->exp_year = (string) $response['source']['expiry_year']; + $payment_meta->brand = (string) $response['source']['scheme']; + $payment_meta->last4 = (string) $response['source']['last4']; $payment_meta->type = (int) GatewayType::CREDIT_CARD; $data = [ 'payment_meta' => $payment_meta, - 'token' => $response->source['id'], + 'token' => $response['source']['id'], 'payment_method_id' => $this->getParent()->payment_hash->data->payment_method_id, ]; - return $this->getParent()->storePaymentMethod($data); + return $this->getParent()->storePaymentMethod($data, ['gateway_customer_reference' => $response['customer']['id']]); } catch (Exception $e) { session()->flash('message', ctrans('texts.payment_method_saving_failed')); } diff --git a/app/PaymentDrivers/CheckoutComPaymentDriver.php b/app/PaymentDrivers/CheckoutComPaymentDriver.php index 1633852f987a..552123dbbc75 100644 --- a/app/PaymentDrivers/CheckoutComPaymentDriver.php +++ b/app/PaymentDrivers/CheckoutComPaymentDriver.php @@ -28,10 +28,22 @@ use App\PaymentDrivers\CheckoutCom\CreditCard; use App\PaymentDrivers\CheckoutCom\Utilities; use App\Utils\Traits\SystemLogTrait; use Checkout\CheckoutApi; +use Checkout\CheckoutApiException; +use Checkout\CheckoutArgumentException; +use Checkout\CheckoutAuthorizationException; +use Checkout\CheckoutDefaultSdk; +use Checkout\CheckoutFourSdk; +use Checkout\Environment; use Checkout\Library\Exceptions\CheckoutHttpException; use Checkout\Models\Payments\IdSource; use Checkout\Models\Payments\Refund; use Exception; +use Checkout\Payments\Four\Request\PaymentRequest; +use Checkout\Payments\Four\Request\Source\RequestIdSource as SourceRequestIdSource; +use Checkout\Payments\PaymentRequest as PaymentsPaymentRequest; +use Checkout\Payments\Source\RequestIdSource; +use Checkout\Common\CustomerRequest; +use Checkout\Payments\RefundRequest; class CheckoutComPaymentDriver extends BaseDriver { @@ -52,6 +64,8 @@ class CheckoutComPaymentDriver extends BaseDriver /* Authorise payment methods */ public $can_authorise_credit_card = true; + public $is_four_api = false; + /** * @var CheckoutApi; */ @@ -109,7 +123,22 @@ class CheckoutComPaymentDriver extends BaseDriver 'sandbox' => $this->company_gateway->getConfigField('testMode'), ]; - $this->gateway = new CheckoutApi($config['secret'], $config['sandbox'], $config['public']); + + if(strlen($config['secret']) <= 38){ + $this->is_four_api = true; + $builder = CheckoutFourSdk::staticKeys(); + $builder->setPublicKey($config['public']); // optional, only required for operations related with tokens + $builder->setSecretKey($config['secret']); + $builder->setEnvironment($config['sandbox'] ? Environment::sandbox(): Environment::production()); + $this->gateway = $builder->build(); + } + else { + $builder = CheckoutDefaultSdk::staticKeys(); + $builder->setPublicKey($config['public']); // optional, only required for operations related with tokens + $builder->setSecretKey($config['secret']); + $builder->setEnvironment($config['sandbox'] ? Environment::sandbox(): Environment::production()); + $this->gateway = $builder->build(); + } return $this; } @@ -121,9 +150,6 @@ class CheckoutComPaymentDriver extends BaseDriver */ public function viewForType($gateway_type_id) { - // At the moment Checkout.com payment - // driver only supports payments using credit card. - return 'gateways.checkout.credit_card.pay'; } @@ -211,22 +237,40 @@ class CheckoutComPaymentDriver extends BaseDriver { $this->init(); - $checkout_payment = new Refund($payment->transaction_reference); + $request = new RefundRequest(); + $request->reference = "{$payment->transaction_reference} " . now(); + $request->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode()); try { - $refund = $this->gateway->payments()->refund($checkout_payment); - $checkout_payment = $this->gateway->payments()->details($refund->id); - - $response = ['refund_response' => $refund, 'checkout_payment_fetch' => $checkout_payment]; + // or, refundPayment("payment_id") for a full refund + $response = $this->gateway->getPaymentsClient()->refundPayment($payment->transaction_reference, $request); return [ - 'transaction_reference' => $refund->action_id, + 'transaction_reference' => $response['action_id'], 'transaction_response' => json_encode($response), - 'success' => $checkout_payment->status == 'Refunded', - 'description' => $checkout_payment->status, - 'code' => $checkout_payment->http_code, + 'success' => true, + 'description' => $response['reference'], + 'code' => 202, ]; - } catch (CheckoutHttpException $e) { + + } catch (CheckoutApiException $e) { + // API error + $request_id = $e->request_id; + $http_status_code = $e->http_status_code; + $error_details = $e->error_details; + } catch (CheckoutArgumentException $e) { + // Bad arguments + + return [ + 'transaction_reference' => null, + 'transaction_response' => json_encode($e->getMessage()), + 'success' => false, + 'description' => $e->getMessage(), + 'code' => $e->getCode(), + ]; + } catch (CheckoutAuthorizationException $e) { + // Bad Invalid authorization + return [ 'transaction_reference' => null, 'transaction_response' => json_encode($e->getMessage()), @@ -235,6 +279,49 @@ class CheckoutComPaymentDriver extends BaseDriver 'code' => $e->getCode(), ]; } + + } + + public function getCustomer() + { + try{ + + $response = $this->gateway->getCustomersClient()->get($this->client->present()->email()); + + return $response; + } + catch(\Exception $e){ + + $request = new CustomerRequest(); + $request->email = $this->client->present()->email(); + $request->name = $this->client->present()->name(); + return $request; + } + } + + public function bootTokenRequest($token) + { + + if($this->is_four_api){ + + $token_source = new SourceRequestIdSource(); + $token_source->id = $token; + $request = new PaymentRequest(); + $request->source = $token_source; + + + } + else { + + $token_source = new RequestIdSource(); + $token_source->id = $token; + $request = new PaymentsPaymentRequest(); + $request->source = $token_source; + + } + + return $request; + } public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) @@ -244,27 +331,29 @@ class CheckoutComPaymentDriver extends BaseDriver $this->init(); - $method = new IdSource($cgt->token); - - $payment = new \Checkout\Models\Payments\Payment($method, $this->client->getCurrencyCode()); - $payment->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode()); - $payment->reference = $invoice->number . '-' . now(); + $paymentRequest = $this->bootTokenRequest($cgt->token); + $paymentRequest->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode()); + $paymentRequest->reference = '#' . $invoice->number . ' - ' . now(); + $paymentRequest->customer = $this->getCustomer(); + $paymentRequest->metadata = ['udf1' => "Invoice Ninja"]; + $paymentRequest->currency = $this->client->getCurrencyCode(); $request = new PaymentResponseRequest(); $request->setMethod('POST'); $request->request->add(['payment_hash' => $payment_hash->hash]); try { - $response = $this->gateway->payments()->request($payment); + // $response = $this->gateway->payments()->request($payment); + $response = $this->gateway->getPaymentsClient()->requestPayment($paymentRequest); - if ($response->status == 'Authorized') { + if ($response['status'] == 'Authorized') { $this->confirmGatewayFee($request); $data = [ - 'payment_method' => $response->source['id'], - 'payment_type' => PaymentType::parseCardType(strtolower($response->source['scheme'])), + 'payment_method' => $response['source']['id'], + 'payment_type' => PaymentType::parseCardType(strtolower($response['source']['scheme'])), 'amount' => $amount, - 'transaction_reference' => $response->id, + 'transaction_reference' => $response['id'], ]; $payment = $this->createPayment($data, Payment::STATUS_COMPLETED); @@ -280,10 +369,10 @@ class CheckoutComPaymentDriver extends BaseDriver return $payment; } - if ($response->status == 'Declined') { + if ($response['status'] == 'Declined') { $this->unWindGatewayFees($payment_hash); - $this->sendFailureMail($response->status . " " . $response->response_summary); + $this->sendFailureMail($response['status'] . " " . $response['response_summary']); $message = [ 'server_response' => $response, @@ -300,11 +389,9 @@ class CheckoutComPaymentDriver extends BaseDriver return false; } - } catch (Exception | CheckoutHttpException $e) { + } catch (Exception | CheckoutApiException $e) { $this->unWindGatewayFees($payment_hash); - $message = $e instanceof CheckoutHttpException - ? $e->getBody() - : $e->getMessage(); + $message = $e->getMessage(); $data = [ 'status' => '', @@ -334,20 +421,21 @@ class CheckoutComPaymentDriver extends BaseDriver public function process3dsConfirmation(Checkout3dsRequest $request) { + $this->init(); $this->setPaymentHash($request->getPaymentHash()); try { - $payment = $this->gateway->payments()->details( + $payment = $this->gateway->getPaymentsClient()->getPaymentDetails( $request->query('cko-session-id') ); - if ($payment->approved) { + if ($payment['approved']) { return $this->processSuccessfulPayment($payment); } else { return $this->processUnsuccessfulPayment($payment); } - } catch (CheckoutHttpException | Exception $e) { + } catch (CheckoutApiException | Exception $e) { return $this->processInternallyFailedPayment($this, $e); } } diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index d42bf5e74809..26810875467e 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -245,7 +245,7 @@ class GoCardlessPaymentDriver extends BaseDriver sleep(1); foreach ($request->events as $event) { - if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out' || $event['action'] === 'paid') { + if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out') { nlog("Searching for transaction reference"); diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 3fe1741fd755..e619deb06319 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -60,11 +60,12 @@ use App\Events\Payment\PaymentWasRefunded; use App\Events\Payment\PaymentWasRestored; use App\Events\Payment\PaymentWasUpdated; use App\Events\Payment\PaymentWasVoided; -use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent; +use App\Events\PurchaseOrder\PurchaseOrderWasAccepted; use App\Events\PurchaseOrder\PurchaseOrderWasArchived; use App\Events\PurchaseOrder\PurchaseOrderWasCreated; use App\Events\PurchaseOrder\PurchaseOrderWasDeleted; use App\Events\PurchaseOrder\PurchaseOrderWasEmailed; +use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent; use App\Events\PurchaseOrder\PurchaseOrderWasRestored; use App\Events\PurchaseOrder\PurchaseOrderWasUpdated; use App\Events\PurchaseOrder\PurchaseOrderWasViewed; @@ -179,6 +180,7 @@ 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\PurchaseOrderArchivedActivity; use App\Listeners\PurchaseOrder\PurchaseOrderDeletedActivity; use App\Listeners\PurchaseOrder\PurchaseOrderEmailActivity; @@ -471,6 +473,9 @@ class EventServiceProvider extends ServiceProvider PurchaseOrderWasViewed::class => [ PurchaseOrderViewedActivity::class, ], + PurchaseOrderWasAccepted::class => [ + PurchaseOrderAcceptedActivity::class, + ], CompanyDocumentsDeleted::class => [ DeleteCompanyDocuments::class, ], diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index b7a060640f57..b832d953aa78 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -50,6 +50,8 @@ class RouteServiceProvider extends ServiceProvider $this->mapContactApiRoutes(); + $this->mapVendorsApiRoutes(); + $this->mapClientApiRoutes(); $this->mapShopApiRoutes(); @@ -125,7 +127,7 @@ class RouteServiceProvider extends ServiceProvider protected function mapVendorsApiRoutes() { Route::prefix('') - ->middleware('vendor') + ->middleware('client') ->namespace($this->namespace) ->group(base_path('routes/vendor.php')); } diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index a0a90deb0363..234392313b31 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -341,7 +341,7 @@ class InvoiceService if(Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'); - if(Ninja::isHosted() && Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) { + if(Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) { Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'); } diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index eaa057448dc7..c16c20bfd909 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -164,6 +164,7 @@ class SubscriptionService $recurring_invoice = $this->convertInvoiceToRecurring($client_contact->client_id); $recurring_invoice->next_send_date = now()->addSeconds($this->subscription->trial_duration); + $recurring_invoice->next_send_date_client = now()->addSeconds($this->subscription->trial_duration); $recurring_invoice->backup = 'is_trial'; if(array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) @@ -620,7 +621,9 @@ class SubscriptionService $recurring_invoice = $this->convertInvoiceToRecurring($old_recurring_invoice->client_id); $recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice); $recurring_invoice->next_send_date = now()->format('Y-m-d'); + $recurring_invoice->next_send_date_client = now()->format('Y-m-d'); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); + $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); /* Start the recurring service */ $recurring_invoice->service() @@ -754,8 +757,9 @@ class SubscriptionService $recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill); $recurring_invoice->due_date_days = 'terms'; $recurring_invoice->next_send_date = now()->format('Y-m-d'); + $recurring_invoice->next_send_date_client = now()->format('Y-m-d'); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); - + $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); return $recurring_invoice; } diff --git a/app/Services/Subscription/ZeroCostProduct.php b/app/Services/Subscription/ZeroCostProduct.php index a926d3d75b85..0d1f8def7850 100644 --- a/app/Services/Subscription/ZeroCostProduct.php +++ b/app/Services/Subscription/ZeroCostProduct.php @@ -58,7 +58,10 @@ class ZeroCostProduct extends AbstractService $recurring_invoice->next_send_date = now(); $recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice); + $recurring_invoice->next_send_date = now(); + $recurring_invoice->next_send_date_client = now(); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); + $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); /* Start the recurring service */ $recurring_invoice->service() diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index e1a9ee0d13eb..554e33ab27f2 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -29,6 +29,7 @@ use App\Models\Payment; use App\Models\PaymentTerm; use App\Models\Product; use App\Models\Project; +use App\Models\PurchaseOrder; use App\Models\Quote; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; @@ -39,6 +40,7 @@ use App\Models\TaskStatus; use App\Models\TaxRate; use App\Models\User; use App\Models\Webhook; +use App\Transformers\PurchaseOrderTransformer; use App\Transformers\RecurringExpenseTransformer; use App\Utils\Traits\MakesHash; use stdClass; @@ -95,6 +97,7 @@ class CompanyTransformer extends EntityTransformer 'task_statuses', 'subscriptions', 'recurring_expenses', + 'purchase_orders', ]; /** @@ -391,4 +394,11 @@ class CompanyTransformer extends EntityTransformer return $this->includeCollection($company->subscriptions, $transformer, Subscription::class); } + + public function includePurchaseOrders(Company $company) + { + $transformer = new PurchaseOrderTransformer($this->serializer); + + return $this->includeCollection($company->purchase_orders, $transformer, PurchaseOrder::class); + } } diff --git a/app/Utils/Traits/Notifications/UserNotifies.php b/app/Utils/Traits/Notifications/UserNotifies.php index 3356a1b9f0f5..3995a4f21517 100644 --- a/app/Utils/Traits/Notifications/UserNotifies.php +++ b/app/Utils/Traits/Notifications/UserNotifies.php @@ -15,6 +15,7 @@ use App\Models\Client; use App\Models\Credit; use App\Models\Invoice; use App\Models\Payment; +use App\Models\PurchaseOrder; use App\Models\Quote; /** @@ -99,7 +100,10 @@ trait UserNotifies break; case ($entity instanceof Credit): return array_merge($required_permissions, ["all_notifications","all_user_notifications","credit_created_user","credit_sent_user","credit_viewed_user"]); - break; + break; + case ($entity instanceof PurchaseOrder): + return array_merge($required_permissions, ["all_notifications","all_user_notifications","purchase_order_created_user","purchase_order_sent_user","purchase_order_viewed_user"]); + break; default: return []; break; @@ -122,7 +126,10 @@ trait UserNotifies break; case ($entity instanceof Credit): return array_diff($required_permissions, ["all_user_notifications","credit_created_user","credit_sent_user","credit_viewed_user"]); - break; + break; + case ($entity instanceof PurchaseOrder): + return array_diff($required_permissions, ["all_user_notifications","purchase_order_created_user","purchase_order_sent_user","purchase_order_viewed_user"]); + break; default: // code... break; diff --git a/composer.json b/composer.json index 452f41241b6c..bbcce36e61c8 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,12 @@ "Credit card billing", "projects", "tasks", - "freelancer" + "freelancer", + "quotes", + "purchase orders", + "stripe billing", + "invoices", + "subscriptions" ], "license": "Elastic License", "authors": [ @@ -37,7 +42,7 @@ "bacon/bacon-qr-code": "^2.0", "beganovich/snappdf": "^1.7", "braintree/braintree_php": "^6.0", - "checkout/checkout-sdk-php": "^1.0", + "checkout/checkout-sdk-php": "^2.5", "cleverit/ubl_invoice": "^1.3", "coconutcraig/laravel-postmark": "^2.10", "doctrine/dbal": "^3.0", diff --git a/composer.lock b/composer.lock index 7b1b4b4bf873..049fe7120a95 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c9278efe297c252de6bc0b5a48540c0b", + "content-hash": "6845489fdc254427c4536e22f025ff51", "packages": [ { "name": "afosto/yaac", @@ -110,16 +110,16 @@ }, { "name": "apimatic/unirest-php", - "version": "2.2.2", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/apimatic/unirest-php.git", - "reference": "a45c4c71a1ea3659b118042a67cc1b6486bcf03a" + "reference": "52e226fb3b7081dc9ef64aee876142a240a5f0f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/apimatic/unirest-php/zipball/a45c4c71a1ea3659b118042a67cc1b6486bcf03a", - "reference": "a45c4c71a1ea3659b118042a67cc1b6486bcf03a", + "url": "https://api.github.com/repos/apimatic/unirest-php/zipball/52e226fb3b7081dc9ef64aee876142a240a5f0f9", + "reference": "52e226fb3b7081dc9ef64aee876142a240a5f0f9", "shasum": "" }, "require": { @@ -168,9 +168,9 @@ "support": { "email": "opensource@apimatic.io", "issues": "https://github.com/apimatic/unirest-php/issues", - "source": "https://github.com/apimatic/unirest-php/tree/2.2.2" + "source": "https://github.com/apimatic/unirest-php/tree/2.3.0" }, - "time": "2022-03-24T08:19:20+00:00" + "time": "2022-06-15T08:29:49+00:00" }, { "name": "asm/php-ansible", @@ -434,16 +434,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.225.2", + "version": "3.225.5", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "f846724ad842916061127d20da4fe4e129f7d4b8" + "reference": "09b404c6b80b9c31be15fa245e647a2f9fb5e733" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f846724ad842916061127d20da4fe4e129f7d4b8", - "reference": "f846724ad842916061127d20da4fe4e129f7d4b8", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/09b404c6b80b9c31be15fa245e647a2f9fb5e733", + "reference": "09b404c6b80b9c31be15fa245e647a2f9fb5e733", "shasum": "" }, "require": { @@ -451,7 +451,7 @@ "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^5.3.3 || ^6.5.6 || ^7.4.3", + "guzzlehttp/guzzle": "^5.3.3 || ^6.2.1 || ^7.0", "guzzlehttp/promises": "^1.4.0", "guzzlehttp/psr7": "^1.7.0 || ^2.1.1", "mtdowling/jmespath.php": "^2.6", @@ -519,9 +519,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.225.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.225.5" }, - "time": "2022-06-10T19:03:26+00:00" + "time": "2022-06-15T19:35:13+00:00" }, { "name": "bacon/bacon-qr-code", @@ -741,28 +741,37 @@ }, { "name": "checkout/checkout-sdk-php", - "version": "1.0.19", + "version": "2.5.1", "source": { "type": "git", "url": "https://github.com/checkout/checkout-sdk-php.git", - "reference": "c2c323ea2f95f74bff3055c42801e7ce22999791" + "reference": "097b862487f7583fd0fab47a08e3dc0800f5c3e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/c2c323ea2f95f74bff3055c42801e7ce22999791", - "reference": "c2c323ea2f95f74bff3055c42801e7ce22999791", + "url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/097b862487f7583fd0fab47a08e3dc0800f5c3e4", + "reference": "097b862487f7583fd0fab47a08e3dc0800f5c3e4", "shasum": "" }, "require": { - "php": ">=5.4.0" + "ext-fileinfo": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5 || ^7.4", + "monolog/monolog": "^1.27 || ^2.4", + "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "^6" + "mockery/mockery": "^1.3 || ^1.4", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0", + "smgladkovskiy/phpcs-git-pre-commit": "dev-master", + "squizlabs/php_codesniffer": "^3.3", + "symfony/phpunit-bridge": "^5.2 || ^6.0" }, "type": "library", "autoload": { "psr-4": { - "Checkout\\": "src/" + "Checkout\\": "lib/Checkout" } }, "notification-url": "https://packagist.org/downloads/", @@ -772,7 +781,7 @@ "authors": [ { "name": "Checkout.com", - "email": "platforms@checkout.com", + "email": "integrations@checkout.com", "homepage": "https://github.com/checkout/checkout-sdk-php/graphs/contributors" } ], @@ -794,9 +803,9 @@ ], "support": { "issues": "https://github.com/checkout/checkout-sdk-php/issues", - "source": "https://github.com/checkout/checkout-sdk-php/tree/1.0.19" + "source": "https://github.com/checkout/checkout-sdk-php/tree/2.5.1" }, - "time": "2021-11-19T15:08:38+00:00" + "time": "2022-06-13T00:23:23+00:00" }, { "name": "cleverit/ubl_invoice", @@ -2285,16 +2294,16 @@ }, { "name": "google/apiclient-services", - "version": "v0.252.0", + "version": "v0.253.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client-services.git", - "reference": "9941c959f6a1f781e49019b78f453d54554dff73" + "reference": "70c62b17f7821526cb52c6f125254dc51f256109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/9941c959f6a1f781e49019b78f453d54554dff73", - "reference": "9941c959f6a1f781e49019b78f453d54554dff73", + "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/70c62b17f7821526cb52c6f125254dc51f256109", + "reference": "70c62b17f7821526cb52c6f125254dc51f256109", "shasum": "" }, "require": { @@ -2323,22 +2332,22 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client-services/issues", - "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.252.0" + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.253.0" }, - "time": "2022-06-06T01:20:11+00:00" + "time": "2022-06-13T01:06:12+00:00" }, { "name": "google/auth", - "version": "v1.21.0", + "version": "v1.21.1", "source": { "type": "git", "url": "https://github.com/googleapis/google-auth-library-php.git", - "reference": "73392bad2eb6852eea9084b6bbdec752515cb849" + "reference": "aa3b9ca29258ac6347ce3c8937a2418c5d78f840" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/73392bad2eb6852eea9084b6bbdec752515cb849", - "reference": "73392bad2eb6852eea9084b6bbdec752515cb849", + "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/aa3b9ca29258ac6347ce3c8937a2418c5d78f840", + "reference": "aa3b9ca29258ac6347ce3c8937a2418c5d78f840", "shasum": "" }, "require": { @@ -2381,9 +2390,9 @@ "support": { "docs": "https://googleapis.github.io/google-auth-library-php/main/", "issues": "https://github.com/googleapis/google-auth-library-php/issues", - "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.21.0" + "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.21.1" }, - "time": "2022-04-13T20:35:52+00:00" + "time": "2022-05-16T19:34:15+00:00" }, { "name": "graham-campbell/result-type", @@ -6016,16 +6025,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.6.1", + "version": "v2.6.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "d6e1d5d0fb2458dfdd7018ec2f74be120353a3b9" + "reference": "58c3f47f650c94ec05a151692652a868995d2938" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d6e1d5d0fb2458dfdd7018ec2f74be120353a3b9", - "reference": "d6e1d5d0fb2458dfdd7018ec2f74be120353a3b9", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", "shasum": "" }, "require": { @@ -6079,7 +6088,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2022-06-11T00:43:46+00:00" + "time": "2022-06-14T06:56:20+00:00" }, { "name": "paragonie/random_compat", @@ -6815,16 +6824,16 @@ }, { "name": "pragmarx/google2fa", - "version": "8.0.0", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b" + "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/26c4c5cf30a2844ba121760fd7301f8ad240100b", - "reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3", + "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3", "shasum": "" }, "require": { @@ -6861,9 +6870,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/8.0.0" + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1" }, - "time": "2020-04-05T10:47:18+00:00" + "time": "2022-06-13T21:57:56+00:00" }, { "name": "predis/predis", diff --git a/config/ninja.php b/config/ninja.php index c07e03da7a42..19bbfb180258 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.3.100', - 'app_tag' => '5.3.100', + 'app_version' => '5.4.0', + 'app_tag' => '5.4.0', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/public/js/clients/purchase_orders/accept.js b/public/js/clients/purchase_orders/accept.js new file mode 100644 index 000000000000..46ec071e7531 --- /dev/null +++ b/public/js/clients/purchase_orders/accept.js @@ -0,0 +1,2 @@ +/*! For license information please see accept.js.LICENSE.txt */ +(()=>{function e(e,t){for(var n=0;n{function e(e,n){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,n){if(!e)return;if("string"==typeof e)return t(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return t(e,n)}(e))||n&&e&&"number"==typeof e.length){r&&(e=r);var o=0,c=function(){};return{s:c,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,l=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return a=e.done,e},e:function(e){l=!0,i=e},f:function(){try{a||null==r.return||r.return()}finally{if(l)throw i}}}}function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&void 0!==arguments[2]?arguments[2]:{};if(r.hasOwnProperty("single")&&document.querySelectorAll(".child-hidden-input").forEach((function(e){return e.remove()})),!1!==t.checked){var o=document.createElement("INPUT");o.setAttribute("name","purchase_orders[]"),o.setAttribute("value",t.dataset.value),o.setAttribute("class","child-hidden-input"),o.hidden=!0,n.append(o)}else{var c,i=document.querySelectorAll("input.child-hidden-input"),a=e(i);try{for(a.s();!(c=a.n()).done;){var l=c.value;l.value==t.dataset.value&&l.remove()}}catch(e){a.e(e)}finally{a.f()}}}},{key:"handle",value:function(){var t=this;this.parentElement.addEventListener("click",(function(){t.watchCheckboxes(t.parentElement)}));var n,r=e(document.querySelectorAll(".form-check-child"));try{var o=function(){var e=n.value;e.addEventListener("click",(function(){t.processChildItem(e,t.parentForm)}))};for(r.s();!(n=r.n()).done;)o()}catch(e){r.e(e)}finally{r.f()}}}],o&&n(r.prototype,o),c&&n(r,c),Object.defineProperty(r,"prototype",{writable:!1}),t}())).handle()})(); \ No newline at end of file diff --git a/public/js/clients/purchase_orders/action-selectors.js.LICENSE.txt b/public/js/clients/purchase_orders/action-selectors.js.LICENSE.txt new file mode 100644 index 000000000000..82efe1d67ef8 --- /dev/null +++ b/public/js/clients/purchase_orders/action-selectors.js.LICENSE.txt @@ -0,0 +1,9 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index b0caacb3cffa..6b0b49fce399 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -4,6 +4,8 @@ "/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=803182f668c39d631ca5c55437876da4", "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=7bed15f51bca764378d9a3aa605b8664", "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=d4f86ddee4e8a1d6e9719010aa0fe62b", + "/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js?id=160b8161599fc2429b449b0970d3ba6c", + "/js/clients/purchase_orders/accept.js": "/js/clients/purchase_orders/accept.js?id=2b5fed3ae34a6fd4db171a77ba72496e", "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=b88ad7c8881cc87df07b129c5a7c76df", "/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=1c5493a4c53a5b862d07ee1818179ea9", "/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=0274ab4f8d2b411f2a2fe5142301e7af", diff --git a/resources/js/clients/purchase_orders/accept.js b/resources/js/clients/purchase_orders/accept.js new file mode 100644 index 000000000000..4d64ffaba6c8 --- /dev/null +++ b/resources/js/clients/purchase_orders/accept.js @@ -0,0 +1,103 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +class Accept { + constructor(displaySignature, displayTerms) { + this.shouldDisplaySignature = displaySignature; + this.shouldDisplayTerms = displayTerms; + this.termsAccepted = false; + } + + submitForm() { + document.getElementById('approve-form').submit(); + } + + displaySignature() { + let displaySignatureModal = document.getElementById( + 'displaySignatureModal' + ); + displaySignatureModal.removeAttribute('style'); + + const signaturePad = new SignaturePad( + document.getElementById('signature-pad'), + { + penColor: 'rgb(0, 0, 0)', + } + ); + + this.signaturePad = signaturePad; + } + + displayTerms() { + let displayTermsModal = document.getElementById("displayTermsModal"); + displayTermsModal.removeAttribute("style"); + } + + handle() { + document + .getElementById('approve-button') + .addEventListener('click', () => { + if (this.shouldDisplaySignature && this.shouldDisplayTerms) { + this.displaySignature(); + + document + .getElementById('signature-next-step') + .addEventListener('click', () => { + this.displayTerms(); + + document + .getElementById('accept-terms-button') + .addEventListener('click', () => { + document.querySelector( + 'input[name="signature"' + ).value = this.signaturePad.toDataURL(); + this.termsAccepted = true; + this.submitForm(); + }); + }); + } + + if (this.shouldDisplaySignature && !this.shouldDisplayTerms) { + this.displaySignature(); + + document + .getElementById('signature-next-step') + .addEventListener('click', () => { + document.querySelector( + 'input[name="signature"' + ).value = this.signaturePad.toDataURL(); + this.submitForm(); + }); + } + + if (!this.shouldDisplaySignature && this.shouldDisplayTerms) { + this.displayTerms(); + + document + .getElementById('accept-terms-button') + .addEventListener('click', () => { + this.termsAccepted = true; + this.submitForm(); + }); + } + + if (!this.shouldDisplaySignature && !this.shouldDisplayTerms) { + this.submitForm(); + } + }); + } +} + +const signature = document.querySelector('meta[name="require-purchase_order-signature"]') + .content; + +const terms = document.querySelector('meta[name="show-purchase_order-terms"]').content; + +new Accept(Boolean(+signature), Boolean(+terms)).handle(); diff --git a/resources/js/clients/purchase_orders/action-selectors.js b/resources/js/clients/purchase_orders/action-selectors.js new file mode 100644 index 000000000000..360261e92b91 --- /dev/null +++ b/resources/js/clients/purchase_orders/action-selectors.js @@ -0,0 +1,79 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +class ActionSelectors { + constructor() { + this.parentElement = document.querySelector('.form-check-parent'); + this.parentForm = document.getElementById('bulkActions'); + } + + watchCheckboxes(parentElement) { + document + .querySelectorAll('.child-hidden-input') + .forEach((element) => element.remove()); + + document.querySelectorAll('.form-check-child').forEach((child) => { + if (parentElement.checked) { + child.checked = parentElement.checked; + this.processChildItem( + child, + document.getElementById('bulkActions') + ); + } else { + child.checked = false; + document + .querySelectorAll('.child-hidden-input') + .forEach((element) => element.remove()); + } + }); + } + + processChildItem(element, parent, options = {}) { + if (options.hasOwnProperty('single')) { + document + .querySelectorAll('.child-hidden-input') + .forEach((element) => element.remove()); + } + + if (element.checked === false) { + let inputs = document.querySelectorAll('input.child-hidden-input'); + + for (let i of inputs) { + if (i.value == element.dataset.value) i.remove(); + } + + return; + } + + let _temp = document.createElement('INPUT'); + + _temp.setAttribute('name', 'purchase_orders[]'); + _temp.setAttribute('value', element.dataset.value); + _temp.setAttribute('class', 'child-hidden-input'); + _temp.hidden = true; + + parent.append(_temp); + } + + handle() { + this.parentElement.addEventListener('click', () => { + this.watchCheckboxes(this.parentElement); + }); + + for (let child of document.querySelectorAll('.form-check-child')) { + child.addEventListener('click', () => { + this.processChildItem(child, this.parentForm); + }); + } + } +} + +/** @handle **/ +new ActionSelectors().handle(); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index ff9b7280a44d..9c044f987481 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4623,7 +4623,14 @@ $LANG = array( 'purchase_order_message' => 'To view your purchase order for :amount, click the link below.', 'view_purchase_order' => 'View Purchase Order', 'purchase_orders_backup_subject' => 'Your purchase orders are ready for download', - + 'notification_purchase_order_viewed_subject' => 'Purchase Order :invoice was viewed by :client', + 'notification_purchase_order_viewed' => 'The following vendor :client viewed Purchase Order :invoice for :amount.', + 'purchase_order_date' => 'Purchase Order Date', + 'purchase_orders' => 'Purchase Orders', + 'purchase_order_number_placeholder' => 'Purchase Order # :purchase_order', + 'accepted' => 'Accepted', + 'activity_137' => ':contact accepted purchase order :purchase_order', + 'vendor_information' => 'Vendor Information', ); return $LANG; diff --git a/resources/views/portal/ninja2020/components/general/sidebar/vendor_desktop.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/vendor_desktop.blade.php new file mode 100644 index 000000000000..9a688332f97d --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/sidebar/vendor_desktop.blade.php @@ -0,0 +1,76 @@ + diff --git a/resources/views/portal/ninja2020/components/general/sidebar/vendor_header.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/vendor_header.blade.php new file mode 100644 index 000000000000..9713ab52b757 --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/sidebar/vendor_header.blade.php @@ -0,0 +1,42 @@ +
+ +
+ @yield('meta_title') +
+ +
+
+ +
+ +
+
+
+
diff --git a/resources/views/portal/ninja2020/components/general/sidebar/vendor_main.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/vendor_main.blade.php new file mode 100644 index 000000000000..f80299580ec2 --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/sidebar/vendor_main.blade.php @@ -0,0 +1,45 @@ +
+ + @if($settings && $settings->enable_client_portal) + + @include('portal.ninja2020.components.general.sidebar.vendor_mobile', ['sidebar' => $sidebar]) + + + @unless(request()->query('sidebar') === 'hidden') + @include('portal.ninja2020.components.general.sidebar.vendor_desktop', ['sidebar' => $sidebar]) + @endunless + @endif + +
+ @if($settings && $settings->enable_client_portal) + @include('portal.ninja2020.components.general.sidebar.vendor_header', ['sidebar' => $sidebar]) + @endif + +
+ +
+ @yield('header') +
+ +
+
+ @includeWhen(session()->has('success'), 'portal.ninja2020.components.general.messages.success') + + {{ $slot }} +
+
+
+ @include('portal.ninja2020.components.general.vendor_footer') +
+
+ + diff --git a/resources/views/portal/ninja2020/components/general/sidebar/vendor_mobile.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/vendor_mobile.blade.php new file mode 100644 index 000000000000..d2d419f1d6e0 --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/sidebar/vendor_mobile.blade.php @@ -0,0 +1,65 @@ +
+
+
+
+ +
+
+ {{ auth()->guard('vendor')->user()->company->present()->name() }} logo +
+ +
+
+
diff --git a/resources/views/portal/ninja2020/components/general/vendor_footer.blade.php b/resources/views/portal/ninja2020/components/general/vendor_footer.blade.php new file mode 100644 index 000000000000..43d1db33b8d2 --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/vendor_footer.blade.php @@ -0,0 +1,48 @@ +
+
+ @if(auth()->guard('vendor')->user() && auth()->guard('vendor')->user()->user->account->isPaid()) + {{ ctrans('texts.footer_label', ['company' => auth()->guard('vendor')->user()->vendor->company->present()->name(), 'year' => date('Y')]) }} + @else + + {{ ctrans('texts.copyright') }} © {{ date('Y') }} + Invoice Ninja. + + @endif + +
+ @if(strlen($settings->client_portal_privacy_policy) > 1) + {{ __('texts.privacy_policy')}} + @endif + + @if(strlen($settings->client_portal_privacy_policy) > 1 && strlen($settings->client_portal_terms) > 1) + + @endif + + @if(strlen($settings->client_portal_terms) > 1) + {{ __('texts.terms')}} + @endif +
+
+ + @if(auth()->guard('vendor')->user()->user && !auth()->guard('vendor')->user()->user->account->isPaid()) + + Invoice Ninja Logo + + @endif + + @if(strlen($settings->client_portal_privacy_policy) > 1) + @component('portal.ninja2020.components.general.pop-up', ['title' => __('texts.privacy_policy') ,'show_property' => 'privacy']) + {!! $settings->client_portal_privacy_policy !!} + @endcomponent + @endif + + @if(strlen($settings->client_portal_terms) > 1) + @component('portal.ninja2020.components.general.pop-up', ['title' => __('texts.terms') ,'show_property' => 'tos']) + {!! $settings->client_portal_terms !!} + @endcomponent + @endif + + +
diff --git a/resources/views/portal/ninja2020/components/livewire/purchase-orders-table.blade.php b/resources/views/portal/ninja2020/components/livewire/purchase-orders-table.blade.php new file mode 100644 index 000000000000..c207f6bc42e3 --- /dev/null +++ b/resources/views/portal/ninja2020/components/livewire/purchase-orders-table.blade.php @@ -0,0 +1,121 @@ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + @forelse($purchase_orders as $purchase_order) + + + + + + + + + + + @empty + + + + @endforelse + +
+ + + + {{ ctrans('texts.purchase_order_number_short') }} + + + + {{ ctrans('texts.purchase_order_date') }} + + + + {{ ctrans('texts.amount') }} + + + + {{ ctrans('texts.balance') }} + + + + {{ ctrans('texts.due_date') }} + + + + {{ ctrans('texts.status') }} + +
+ + + {{ $purchase_order->number }} + + {{ $purchase_order->translateDate($purchase_order->date, $purchase_order->company->date_format(), $purchase_order->company->locale()) }} + + {{ App\Utils\Number::formatMoney($purchase_order->amount, $purchase_order->company) }} + + {{ App\Utils\Number::formatMoney($purchase_order->balance, $purchase_order->company) }} + + {{ $purchase_order->translateDate($purchase_order->due_date, $purchase_order->company->date_format(), $purchase_order->company->locale()) }} + + {!! App\Models\PurchaseOrder::badgeForStatus($purchase_order->status_id) !!} + + + {{ ctrans('texts.view') }} + +
+ {{ ctrans('texts.no_results') }} +
+
+
+
+ @if($purchase_orders && $purchase_orders->total() > 0) + + @endif + {{ $purchase_orders->links('portal/ninja2020/vendor/pagination') }} +
+
+ +@push('footer') + +@endpush diff --git a/resources/views/portal/ninja2020/layout/vendor_app.blade.php b/resources/views/portal/ninja2020/layout/vendor_app.blade.php new file mode 100644 index 000000000000..bed8fbe29558 --- /dev/null +++ b/resources/views/portal/ninja2020/layout/vendor_app.blade.php @@ -0,0 +1,168 @@ + + + + + + + @if (config('services.analytics.tracking_id')) + + + + @else + + @endif + + + @if(isset($company->account) && !$company->account->isPaid()) + @yield('meta_title', '') — Invoice Ninja + @elseif(isset($company) && !is_null($company)) + @yield('meta_title', '') — {{ $company->present()->name() }} + @else + @yield('meta_title', '') + @endif + + + + + + + + + + + + + + + + + + + + @if(auth()->guard('vendor')->user() && !auth()->guard('vendor')->user()->user->account->isPaid()) + + @endif + + + + @if((bool) \App\Utils\Ninja::isSelfHost()) + + @endif + + @livewireStyles + + {{-- Feel free to push anything to header using @push('header') --}} + @stack('head') + + @if((isset($company) && $company->account->isPaid() && !empty($settings->portal_custom_head)) || ((bool) \App\Utils\Ninja::isSelfHost() && !empty($settings->portal_custom_head))) +
+ {!! $settings->portal_custom_head !!} +
+ @endif + + + + + @include('portal.ninja2020.components.primary-color') + + + @if(session()->has('message')) +
+ {{ session('message') }} +
+ @endif + + @component('portal.ninja2020.components.general.sidebar.vendor_main', ['settings' => $settings, 'sidebar' => $sidebar]) + @yield('body') + @endcomponent + + @livewireScripts + + + + + @if($company && $company->google_analytics_key) + + @endif + + + +
+ @yield('footer') + @stack('footer') + + @if((bool) \App\Utils\Ninja::isSelfHost() && !empty($settings->portal_custom_footer)) +
+ {!! $settings->portal_custom_footer !!} +
+ @endif +
+ + @if((bool) \App\Utils\Ninja::isSelfHost()) + + @endif + diff --git a/resources/views/portal/ninja2020/purchase_orders/catch.blade.php b/resources/views/portal/ninja2020/purchase_orders/catch.blade.php new file mode 100644 index 000000000000..dd71106f5169 --- /dev/null +++ b/resources/views/portal/ninja2020/purchase_orders/catch.blade.php @@ -0,0 +1,11 @@ +@extends('portal.ninja2020.layout.clean') +@section('meta_title', ctrans('texts.vendor')) + +@component('portal.ninja2020.components.test') +@endcomponent + +@section('body') +
+

Vendor Portal

+
+@endsection diff --git a/resources/views/portal/ninja2020/purchase_orders/includes/actions.blade.php b/resources/views/portal/ninja2020/purchase_orders/includes/actions.blade.php new file mode 100644 index 000000000000..53b35288b232 --- /dev/null +++ b/resources/views/portal/ninja2020/purchase_orders/includes/actions.blade.php @@ -0,0 +1,40 @@ +
+@csrf + + + + + + +
+
+
+
+ +

+ {{ ctrans('texts.approve') }} +

+ + + +
+ +
+ @yield('quote-not-approved-right-side') + +
+ +
+
+
+
+
+ +
diff --git a/resources/views/portal/ninja2020/purchase_orders/index.blade.php b/resources/views/portal/ninja2020/purchase_orders/index.blade.php new file mode 100644 index 000000000000..12695e68ec78 --- /dev/null +++ b/resources/views/portal/ninja2020/purchase_orders/index.blade.php @@ -0,0 +1,25 @@ +@extends('portal.ninja2020.layout.vendor_app') +@section('meta_title', ctrans('texts.purchase_orders')) + +@section('header') + @if($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif +@endsection + +@section('body') +
+
+ @csrf + + +
+
+
+ @livewire('purchase-orders-table', ['company' => $company]) +
+@endsection diff --git a/resources/views/portal/ninja2020/purchase_orders/show-fullscreen.blade.php b/resources/views/portal/ninja2020/purchase_orders/show-fullscreen.blade.php new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/resources/views/portal/ninja2020/purchase_orders/show.blade.php b/resources/views/portal/ninja2020/purchase_orders/show.blade.php new file mode 100644 index 000000000000..bc2a91e97cd5 --- /dev/null +++ b/resources/views/portal/ninja2020/purchase_orders/show.blade.php @@ -0,0 +1,59 @@ +@extends('portal.ninja2020.layout.vendor_app') +@section('meta_title', ctrans('texts.view_purchase_order')) + +@push('head') + + + @include('portal.ninja2020.components.no-cache') + + + +@endpush + +@section('body') + + @if(in_array($purchase_order->status_id, [\App\Models\PurchaseOrder::STATUS_SENT, \App\Models\PurchaseOrder::STATUS_DRAFT])) +
+ @include('portal.ninja2020.purchase_orders.includes.actions', ['purchase_order' => $purchase_order]) +
+ @else +
+
+
+
+

+ {{ ctrans('texts.purchase_order_number_placeholder', ['purchase_order' => $purchase_order->number])}} + - {{ \App\Models\PurchaseOrder::stringStatus($purchase_order->status_id) }} +

+ + @if($key) + + @endif +
+
+
+
+ @endif + + @include('portal.ninja2020.components.entity-documents', ['entity' => $purchase_order]) + @include('portal.ninja2020.components.pdf-viewer', ['entity' => $purchase_order]) + @include('portal.ninja2020.invoices.includes.terms', ['entities' => [$purchase_order], 'entity_type' => ctrans('texts.purchase_order')]) + @include('portal.ninja2020.invoices.includes.signature') +@endsection + +@section('footer') + + + + +@endsection + diff --git a/routes/vendor.php b/routes/vendor.php index 48a8af4bf0cf..28986e8ad0ac 100644 --- a/routes/vendor.php +++ b/routes/vendor.php @@ -9,5 +9,35 @@ | is assigned the "api" middleware group. Enjoy building your API! | */ +use App\Http\Controllers\Auth\VendorContactLoginController; +use App\Http\Controllers\VendorPortal\InvitationController; +use App\Http\Controllers\VendorPortal\PurchaseOrderController; +use App\Http\Controllers\VendorPortal\VendorContactController; +use Illuminate\Support\Facades\Route; + +Route::get('vendors', [VendorContactLoginController::class, 'catch'])->name('vendor.catchall')->middleware(['domain_db', 'contact_account','vendor_locale']); //catch all + +Route::group(['middleware' => ['invite_db'], 'prefix' => 'vendor', 'as' => 'vendor.'], function () { + /*Invitation catches*/ + Route::get('purchase_order/{invitation_key}', [InvitationController::class, 'purchaseOrder']); + // Route::get('purchase_order/{invitation_key}/download_pdf', 'PurchaseOrderController@downloadPdf')->name('recurring_invoice.download_invitation_key'); + // Route::get('purchase_order/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload'); + +}); +Route::group(['middleware' => ['auth:vendor', 'vendor_locale', 'domain_db'], 'prefix' => 'vendor', 'as' => 'vendor.'], function () { + + Route::get('dashboard', [PurchaseOrderController::class, 'index'])->name('dashboard'); + Route::get('purchase_orders', [PurchaseOrderController::class, 'index'])->name('purchase_orders.index'); + Route::get('purchase_orders/{purchase_order}', [PurchaseOrderController::class, 'show'])->name('purchase_order.show'); + + Route::get('profile/{vendor_contact}/edit', [VendorContactController::class, 'edit'])->name('profile.edit'); + Route::put('profile/{vendor_contact}/edit', [VendorContactController::class, 'update'])->name('profile.update'); + + Route::post('purchase_orders/bulk', [PurchaseOrderController::class, 'bulk'])->name('purchase_orders.bulk'); + Route::get('logout', [VendorContactLoginController::class, 'logout'])->name('logout'); + +}); + +Route::fallback('BaseController@notFoundVendor'); \ No newline at end of file diff --git a/tests/Feature/Inventory/InventoryManagementTest.php b/tests/Feature/Inventory/InventoryManagementTest.php index 8415e6cdc29a..567a2bf917b4 100644 --- a/tests/Feature/Inventory/InventoryManagementTest.php +++ b/tests/Feature/Inventory/InventoryManagementTest.php @@ -85,27 +85,27 @@ class InventoryManagementTest extends TestCase $this->assertEquals(90, $product->in_stock_quantity); - $arr = $response->json(); - $invoice_hashed_id = $arr['data']['id']; + // $arr = $response->json(); + // $invoice_hashed_id = $arr['data']['id']; - $invoice_item = new InvoiceItem; - $invoice_item->type_id = 1; - $invoice_item->product_key = $product->product_key; - $invoice_item->notes = $product->notes; - $invoice_item->quantity = 5; - $invoice_item->cost = 100; + // $invoice_item = new InvoiceItem; + // $invoice_item->type_id = 1; + // $invoice_item->product_key = $product->product_key; + // $invoice_item->notes = $product->notes; + // $invoice_item->quantity = 5; + // $invoice_item->cost = 100; - $line_items2[] = $invoice_item; - $invoice->line_items = $line_items2; + // $line_items2[] = $invoice_item; + // $invoice->line_items = $line_items2; - $response = $this->withHeaders([ - 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, - ])->put('/api/v1/invoices/'.$invoice_hashed_id, $invoice->toArray()) - ->assertStatus(200); + // $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $this->token, + // ])->put('/api/v1/invoices/'.$invoice_hashed_id, $invoice->toArray()) + // ->assertStatus(200); - $product = $product->refresh(); + // $product = $product->refresh(); - $this->assertEquals(95, $product->in_stock_quantity); + // $this->assertEquals(95, $product->in_stock_quantity); } } diff --git a/webpack.mix.js b/webpack.mix.js index 9f0c809188b5..926ac4b4000a 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -18,6 +18,14 @@ mix.js("resources/js/app.js", "public/js") "resources/js/clients/invoices/action-selectors.js", "public/js/clients/invoices/action-selectors.js" ) + .js( + "resources/js/clients/purchase_orders/action-selectors.js", + "public/js/clients/purchase_orders/action-selectors.js" + ) + .js( + "resources/js/clients/purchase_orders/accept.js", + "public/js/clients/purchase_orders/accept.js" + ) .js( "resources/js/clients/invoices/payment.js", "public/js/clients/invoices/payment.js"