From fba37171aebf67d62c368deaf1ac47bcf5aa9322 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 12:08:41 +0300 Subject: [PATCH 01/17] Support query counter in webapp as well as API --- app/Http/Controllers/BaseAPIController.php | 38 ++++++++-------------- app/Http/Kernel.php | 1 + app/Http/Middleware/QueryLogging.php | 38 ++++++++++++++++++++++ 3 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 app/Http/Middleware/QueryLogging.php diff --git a/app/Http/Controllers/BaseAPIController.php b/app/Http/Controllers/BaseAPIController.php index 0718e95b91e4..4660e9b25e4c 100644 --- a/app/Http/Controllers/BaseAPIController.php +++ b/app/Http/Controllers/BaseAPIController.php @@ -61,40 +61,36 @@ class BaseAPIController extends Controller } $this->serializer = Request::get('serializer') ?: API_SERIALIZER_ARRAY; - + if ($this->serializer === API_SERIALIZER_JSON) { $this->manager->setSerializer(new JsonApiSerializer()); } else { $this->manager->setSerializer(new ArraySerializer()); } - - if (Utils::isNinjaDev()) { - \DB::enableQueryLog(); - } } protected function handleAction($request) - { + { $entity = $request->entity(); $action = $request->action; - + $repo = Utils::toCamelCase($this->entityType) . 'Repo'; - + $this->$repo->$action($entity); - + return $this->itemResponse($entity); } protected function listResponse($query) { $transformerClass = EntityModel::getTransformerName($this->entityType); - $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); + $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); $includes = $transformer->getDefaultIncludes(); $includes = $this->getRequestIncludes($includes); $query->with($includes); - + if ($updatedAt = Input::get('updated_at')) { $updatedAt = date('Y-m-d H:i:s', $updatedAt); $query->where(function($query) use ($includes, $updatedAt) { @@ -106,14 +102,14 @@ class BaseAPIController extends Controller } }); } - + if ($clientPublicId = Input::get('client_id')) { $filter = function($query) use ($clientPublicId) { $query->where('public_id', '=', $clientPublicId); }; $query->whereHas('client', $filter); } - + if ( ! Utils::hasPermission('view_all')){ if ($this->entityType == ENTITY_USER) { $query->where('id', '=', Auth::user()->id); @@ -121,7 +117,7 @@ class BaseAPIController extends Controller $query->where('user_id', '=', Auth::user()->id); } } - + $data = $this->createCollection($query, $transformer, $this->entityType); return $this->response($data); @@ -130,10 +126,10 @@ class BaseAPIController extends Controller protected function itemResponse($item) { $transformerClass = EntityModel::getTransformerName($this->entityType); - $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); + $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); $data = $this->createItem($item, $transformer, $this->entityType); - + return $this->response($data); } @@ -160,18 +156,12 @@ class BaseAPIController extends Controller } else { $resource = new Collection($query, $transformer, $entityType); } - + return $this->manager->createData($resource)->toArray(); } protected function response($response) { - if (Utils::isNinjaDev()) { - $count = count(\DB::getQueryLog()); - Log::info(Request::method() . ' - ' . Request::url() . ": $count queries"); - Log::info(json_encode(\DB::getQueryLog())); - } - $index = Request::get('index') ?: 'data'; if ($index == 'none') { @@ -222,7 +212,7 @@ class BaseAPIController extends Controller $data[] = $include; } } - + return $data; } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 1142338b200e..8f7db1f0fabd 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -17,6 +17,7 @@ class Kernel extends HttpKernel { 'Illuminate\View\Middleware\ShareErrorsFromSession', 'App\Http\Middleware\VerifyCsrfToken', 'App\Http\Middleware\DuplicateSubmissionCheck', + 'App\Http\Middleware\QueryLogging', 'App\Http\Middleware\StartupCheck', ]; diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php new file mode 100644 index 000000000000..da01a52b05fd --- /dev/null +++ b/app/Http/Middleware/QueryLogging.php @@ -0,0 +1,38 @@ +url(), '_debugbar') === false) { + $queries = DB::getQueryLog(); + $count = count($queries); + Log::info($request->method() . ' - ' . $request->url() . ": $count queries"); + //Log::info(json_encode($queries)); + } + } + + return $response; + } +} From f873998e22a259825d875532bd8d0d6d260cd563 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 12:23:13 +0300 Subject: [PATCH 02/17] Simplified reseller setup --- app/Ninja/Repositories/AccountRepository.php | 74 ++++++++++---------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index 38753dde9b9a..42e04cefe7fc 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -28,7 +28,7 @@ class AccountRepository { $company = new Company(); $company->save(); - + $account = new Account(); $account->ip = Request::getClientIp(); $account->account_key = str_random(RANDOM_KEY_LENGTH); @@ -87,7 +87,7 @@ class AccountRepository private function getAccountSearchData($user) { $account = $user->account; - + $data = [ 'clients' => [], 'contacts' => [], @@ -102,7 +102,7 @@ class AccountRepository if ($account->custom_client_label2) { $data[$account->custom_client_label2] = []; } - + if ($user->hasPermission('view_all')) { $clients = Client::scope() ->with('contacts', 'invoices') @@ -114,7 +114,7 @@ class AccountRepository $query->where('user_id', '=', $user->id); }])->get(); } - + foreach ($clients as $client) { if ($client->name) { $data['clients'][] = [ @@ -122,20 +122,20 @@ class AccountRepository 'tokens' => $client->name, 'url' => $client->present()->url, ]; - } + } if ($client->custom_value1) { $data[$account->custom_client_label1][] = [ 'value' => "{$client->custom_value1}: " . $client->getDisplayName(), 'tokens' => $client->custom_value1, - 'url' => $client->present()->url, + 'url' => $client->present()->url, ]; - } + } if ($client->custom_value2) { $data[$account->custom_client_label2][] = [ 'value' => "{$client->custom_value2}: " . $client->getDisplayName(), 'tokens' => $client->custom_value2, - 'url' => $client->present()->url, + 'url' => $client->present()->url, ]; } @@ -242,9 +242,9 @@ class AccountRepository if ($credit < 0) { $credit = 0; } - + $plan_cost = Account::$plan_prices[$plan][$term]; - + $account = $this->getNinjaAccount(); $lastInvoice = Invoice::withTrashed()->whereAccountId($account->id)->orderBy('public_id', 'DESC')->first(); $publicId = $lastInvoice ? ($lastInvoice->public_id + 1) : 1; @@ -266,28 +266,28 @@ class AccountRepository $credit_item->product_key = trans('texts.plan_credit_product'); $invoice->invoice_items()->save($credit_item); } - + $item = InvoiceItem::createNew($invoice); $item->qty = 1; $item->cost = $plan_cost; $item->notes = trans("texts.{$plan}_plan_{$term}_description"); - + // Don't change this without updating the regex in PaymentService->createPayment() $item->product_key = 'Plan - '.ucfirst($plan).' ('.ucfirst($term).')'; $invoice->invoice_items()->save($item); - + if ($pending_monthly) { $term_end = $term == PLAN_MONTHLY ? date_create('+1 month') : date_create('+1 year'); $pending_monthly_item = InvoiceItem::createNew($invoice); $item->qty = 1; $pending_monthly_item->cost = 0; $pending_monthly_item->notes = trans("texts.plan_pending_monthly", array('date', Utils::dateToString($term_end))); - + // Don't change this without updating the text in PaymentService->createPayment() $pending_monthly_item->product_key = 'Pending Monthly'; $invoice->invoice_items()->save($pending_monthly_item); } - + $invitation = new Invitation(); $invitation->account_id = $account->id; @@ -328,12 +328,14 @@ class AccountRepository $user->notify_paid = true; $account->users()->save($user); - $accountGateway = new AccountGateway(); - $accountGateway->user_id = $user->id; - $accountGateway->gateway_id = NINJA_GATEWAY_ID; - $accountGateway->public_id = 1; - $accountGateway->setConfig(json_decode(env(NINJA_GATEWAY_CONFIG))); - $account->account_gateways()->save($accountGateway); + if ($config = env(NINJA_GATEWAY_CONFIG)) { + $accountGateway = new AccountGateway(); + $accountGateway->user_id = $user->id; + $accountGateway->gateway_id = NINJA_GATEWAY_ID; + $accountGateway->public_id = 1; + $accountGateway->setConfig(json_decode($config)); + $account->account_gateways()->save($accountGateway); + } } return $account; @@ -356,11 +358,11 @@ class AccountRepository $client->user_id = $ninjaUser->id; $client->currency_id = 1; } - + foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id', 'vat_number'] as $field) { $client->$field = $account->$field; } - + $client->save(); if ($clientExists) { @@ -372,7 +374,7 @@ class AccountRepository $contact->public_id = $account->id; $contact->is_primary = true; } - + $user = $account->getPrimaryUser(); foreach (['first_name', 'last_name', 'email', 'phone'] as $field) { $contact->$field = $user->$field; @@ -513,7 +515,7 @@ class AccountRepository if ($with) { $users->with($with); } - + return $users->get(); } @@ -565,7 +567,7 @@ class AccountRepository $record->save(); $users = $this->getUserAccounts($record); - + // Pick the primary user foreach ($users as $user) { if (!$user->public_id) { @@ -573,16 +575,16 @@ class AccountRepository if(empty($primaryUser)) { $useAsPrimary = true; } - + $planDetails = $user->account->getPlanDetails(false, false); $planLevel = 0; - + if ($planDetails) { $planLevel = 1; if ($planDetails['plan'] == PLAN_ENTERPRISE) { $planLevel = 2; } - + if (!$useAsPrimary && ( $planLevel > $primaryUserPlanLevel || ($planLevel == $primaryUserPlanLevel && $planDetails['expires'] > $primaryUserPlanExpires) @@ -590,7 +592,7 @@ class AccountRepository $useAsPrimary = true; } } - + if ($useAsPrimary) { $primaryUser = $user; $primaryUserPlanLevel = $planLevel; @@ -600,14 +602,14 @@ class AccountRepository } } } - + // Merge other companies into the primary user's company if (!empty($primaryUser)) { foreach ($users as $user) { if ($user == $primaryUser || $user->public_id) { continue; } - + if ($user->account->company_id != $primaryUser->account->company_id) { foreach ($user->account->company->accounts as $account) { $account->company_id = $primaryUser->account->company_id; @@ -636,9 +638,9 @@ class AccountRepository $userAccount->removeUserId($userId); $userAccount->save(); } - + $user = User::whereId($userId)->first(); - + if (!$user->public_id && $user->account->company->accounts->count() > 1) { $company = Company::create(); $company->save(); @@ -660,7 +662,7 @@ class AccountRepository ->withTrashed() ->first(); } while ($match); - + return $code; } @@ -668,7 +670,7 @@ class AccountRepository { $name = trim($name) ?: 'TOKEN'; $users = $this->findUsers($user); - + foreach ($users as $user) { if ($token = AccountToken::whereUserId($user->id)->whereName($name)->first()) { continue; From df3a103825dcead9d68a31d490b5d41159fc097a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 15:26:22 +0300 Subject: [PATCH 03/17] Added missing report field --- resources/lang/en/texts.php | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 0aebc8b87903..31847fdd09b5 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1054,14 +1054,14 @@ $LANG = array( 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', 'send_portal_password'=>'Generate password automatically', 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', - + 'expired' => 'Expired', 'invalid_card_number' => 'The credit card number is not valid.', 'invalid_expiry' => 'The expiration date is not valid.', 'invalid_cvv' => 'The CVV is not valid.', 'cost' => 'Cost', 'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.', - + // User Permissions 'owner' => 'Owner', 'administrator' => 'Administrator', @@ -1079,8 +1079,8 @@ $LANG = array( 'create_all_help' => 'Allow user to create and modify records', 'view_all_help' => 'Allow user to view records they didn\'t create', 'edit_all_help' => 'Allow user to modify records they didn\'t create', - 'view_payment' => 'View Payment', - + 'view_payment' => 'View Payment', + 'january' => 'January', 'february' => 'February', 'march' => 'March', @@ -1093,7 +1093,7 @@ $LANG = array( 'october' => 'October', 'november' => 'November', 'december' => 'December', - + // Documents 'documents_header' => 'Documents:', 'email_documents_header' => 'Documents:', @@ -1125,11 +1125,11 @@ $LANG = array( 'enable_client_portal_help' => 'Show/hide the client portal.', 'enable_client_portal_dashboard' => 'Dashboard', 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', - + // Plans 'account_management' => 'Account Management', 'plan_status' => 'Plan Status', - + 'plan_upgrade' => 'Upgrade', 'plan_change' => 'Change Plan', 'pending_change_to' => 'Changes To', @@ -1159,9 +1159,9 @@ $LANG = array( 'plan_paid' => 'Term Started', 'plan_started' => 'Plan Started', 'plan_expires' => 'Plan Expires', - + 'white_label_button' => 'White Label', - + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', 'enterprise_plan_product' => 'Enterprise Plan', @@ -1181,8 +1181,8 @@ $LANG = array( 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', 'return_to_app' => 'Return to app', - - + + // Payment updates 'refund_payment' => 'Refund Payment', 'refund_max' => 'Max:', @@ -1199,7 +1199,7 @@ $LANG = array( 'activity_39' => ':user cancelled a :payment_amount payment (:payment)', 'activity_40' => ':user refunded :adjustment of a :payment_amount payment (:payment)', 'card_expiration' => 'Exp: :expires', - + 'card_creditcardother' => 'Unknown', 'card_americanexpress' => 'American Express', 'card_carteblanche' => 'Carte Blanche', @@ -1251,7 +1251,7 @@ $LANG = array( 'remove' => 'Remove', 'payment_method_removed' => 'Removed payment method.', 'bank_account_verification_help' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. Please enter the amounts below.', - 'bank_account_verification_next_steps' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. + 'bank_account_verification_next_steps' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. Once you have the amounts, come back to this payment methods page and click "Complete Verification" next to the account.', 'unknown_bank' => 'Unknown Bank', 'ach_verification_delay_help' => 'You will be able to use the account after completing verification. Verification usually takes 1-2 business days.', @@ -1293,8 +1293,10 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', + 'chart_type' => 'Chart Type', + ); return $LANG; -?>. \ No newline at end of file +?>. From b111adaf173dd83e1b1103344559ccc27aae5f7c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 15:35:59 +0300 Subject: [PATCH 04/17] Added and removed extra lables from activity messages --- .../Controllers/PublicClientController.php | 89 ++++++++++--------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 405de63493b0..95f350dc04f0 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -60,7 +60,7 @@ class PublicClientController extends BaseController ]); } - if (!Input::has('phantomjs') && !Input::has('silent') && !Session::has($invitationKey) + if (!Input::has('phantomjs') && !Input::has('silent') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { if ($invoice->is_quote) { event(new QuoteInvitationWasViewed($invoice, $invitation)); @@ -73,7 +73,7 @@ class PublicClientController extends BaseController Session::put('invitation_key', $invitationKey); // track current invitation $account->loadLocalizationSettings($client); - + $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); $invoice->features = [ @@ -82,7 +82,7 @@ class PublicClientController extends BaseController 'invoice_settings' => $account->hasFeature(FEATURE_INVOICE_SETTINGS), ]; $invoice->invoice_fonts = $account->getFontsData(); - + if ($invoice->invoice_design_id == CUSTOM_DESIGN) { $invoice->invoice_design->javascript = $account->custom_design; } else { @@ -149,10 +149,10 @@ class PublicClientController extends BaseController 'checkoutComDebug' => $checkoutComDebug, 'phantomjs' => Input::has('phantomjs'), ); - + if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){ $zipDocs = $this->getInvoiceZipDocuments($invoice, $size); - + if(count($zipDocs) > 1){ $data['documentsZipURL'] = URL::to("client/documents/{$invitation->invitation_key}"); $data['documentsZipSize'] = $size; @@ -173,6 +173,7 @@ class PublicClientController extends BaseController foreach ($paymentMethods as $paymentMethod) { if ($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) { $code = htmlentities(str_replace(' ', '', strtolower($paymentMethod->payment_type->name))); + $html = ''; if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { if($paymentMethod->bank_data) { @@ -301,7 +302,7 @@ class PublicClientController extends BaseController $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } } - + return response()->view('invited.dashboard', $data); } @@ -321,9 +322,9 @@ class PublicClientController extends BaseController $data = [ 'client' => Utils::getClientDisplayName($model), 'user' => $model->is_system ? ('' . trans('texts.system') . '') : ($model->user_first_name . ' ' . $model->user_last_name), - 'invoice' => trans('texts.invoice') . ' ' . $model->invoice, + 'invoice' => $model->invoice, 'contact' => Utils::getClientDisplayName($model), - 'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''), + 'payment' => $model->payment ? ' ' . $model->payment : '', 'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '', 'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null, 'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null, @@ -349,7 +350,7 @@ class PublicClientController extends BaseController } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - + $data = [ 'color' => $color, 'account' => $account, @@ -420,7 +421,7 @@ class PublicClientController extends BaseController return $this->returnError(); } - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, 'account' => $account, @@ -469,7 +470,7 @@ class PublicClientController extends BaseController ->orderColumns( 'invoice_number', 'transaction_reference', 'payment_type', 'amount', 'payment_date') ->make(); } - + private function getPaymentStatusLabel($model) { $label = trans("texts.status_" . strtolower($model->payment_status_name)); @@ -544,7 +545,7 @@ class PublicClientController extends BaseController return $this->returnError(); } - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, 'account' => $account, @@ -597,55 +598,55 @@ class PublicClientController extends BaseController return $invitation; } - + public function getDocumentVFSJS($publicId, $name){ if (!$invitation = $this->getInvitation()) { return $this->returnError(); } - + $clientId = $invitation->invoice->client_id; $document = Document::scope($publicId, $invitation->account_id)->first(); - - + + if(!$document->isPDFEmbeddable()){ return Response::view('error', array('error'=>'Image does not exist!'), 404); } - + $authorized = false; if($document->expense && $document->expense->client_id == $invitation->invoice->client_id){ $authorized = true; } else if($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id){ $authorized = true; } - + if(!$authorized){ return Response::view('error', array('error'=>'Not authorized'), 403); - } - + } + if(substr($name, -3)=='.js'){ $name = substr($name, 0, -3); } - + $content = $document->preview?$document->getRawPreview():$document->getRaw(); $content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")'; $response = Response::make($content, 200); $response->header('content-type', 'text/javascript'); $response->header('cache-control', 'max-age=31536000'); - + return $response; } - + protected function canCreateZip(){ return function_exists('gmp_init'); } - + protected function getInvoiceZipDocuments($invoice, &$size=0){ $documents = $invoice->documents; - + foreach($invoice->expenses as $expense){ $documents = $documents->merge($expense->documents); } - + $documents = $documents->sortBy('size'); $size = 0; @@ -653,16 +654,16 @@ class PublicClientController extends BaseController $toZip = array(); foreach($documents as $document){ if($size + $document->size > $maxSize)break; - + if(!empty($toZip[$document->name])){ // This name is taken if($toZip[$document->name]->hash != $document->hash){ // 2 different files with the same name $nameInfo = pathinfo($document->name); - + for($i = 1;; $i++){ $name = $nameInfo['filename'].' ('.$i.').'.$nameInfo['extension']; - + if(empty($toZip[$name])){ $toZip[$name] = $document; $size += $document->size; @@ -672,7 +673,7 @@ class PublicClientController extends BaseController break; } } - + } } else{ @@ -680,25 +681,25 @@ class PublicClientController extends BaseController $size += $document->size; } } - + return $toZip; } - + public function getInvoiceDocumentsZip($invitationKey){ if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { return $this->returnError(); } - + Session::put('invitation_key', $invitationKey); // track current invitation - + $invoice = $invitation->invoice; - + $toZip = $this->getInvoiceZipDocuments($invoice); - + if(!count($toZip)){ return Response::view('error', array('error'=>'No documents small enough'), 404); } - + $zip = new ZipArchive($invitation->account->name.' Invoice '.$invoice->invoice_number.'.zip'); return Response::stream(function() use ($toZip, $zip) { foreach($toZip as $name=>$document){ @@ -716,28 +717,28 @@ class PublicClientController extends BaseController $zip->finish(); }, 200); } - + public function getDocument($invitationKey, $publicId){ if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { return $this->returnError(); } - + Session::put('invitation_key', $invitationKey); // track current invitation - + $clientId = $invitation->invoice->client_id; $document = Document::scope($publicId, $invitation->account_id)->firstOrFail(); - + $authorized = false; if($document->expense && $document->expense->client_id == $invitation->invoice->client_id){ $authorized = true; } else if($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id){ $authorized = true; } - + if(!$authorized){ return Response::view('error', array('error'=>'Not authorized'), 403); - } - + } + return DocumentController::getDownloadResponse($document); } From 32ab340b74ce9dd608e9244d8230bf88830c8273 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 16:00:07 +0300 Subject: [PATCH 05/17] Added fake card details for test payment --- resources/views/payments/add_paymentmethod.blade.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php index 7c5ca2b956cf..376230315322 100644 --- a/resources/views/payments/add_paymentmethod.blade.php +++ b/resources/views/payments/add_paymentmethod.blade.php @@ -299,6 +299,15 @@ {{ Former::populateField('state', 'NY') }} {{ Former::populateField('postal_code', '10118') }} {{ Former::populateField('country_id', 840) }} + + @endif @@ -694,4 +703,4 @@ @endif -@stop \ No newline at end of file +@stop From 52fe1b0decb3443a972b980a940812793f4e8d06 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 15 May 2016 10:06:14 +0300 Subject: [PATCH 06/17] Added missing text value --- resources/lang/en/texts.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 31847fdd09b5..5ccec0032495 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1294,6 +1294,7 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', 'chart_type' => 'Chart Type', + 'format' => 'Format', ); From b067697b1ca1e84a031705757af90a31e7779c8a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 15 May 2016 13:58:11 +0300 Subject: [PATCH 07/17] Support manually importing OFX files --- app/Http/Controllers/AccountController.php | 14 +-- .../Controllers/BankAccountController.php | 26 ++++++ app/Http/Controllers/BaseController.php | 2 +- app/Http/routes.php | 26 +++--- app/Libraries/OFX.php | 10 +- app/Models/Expense.php | 15 ++- app/Services/BankAccountService.php | 92 ++++++++++++------- resources/lang/en/texts.php | 4 +- .../views/accounts/bank_account.blade.php | 31 ++++--- resources/views/accounts/banks.blade.php | 40 ++++---- resources/views/accounts/import_ofx.blade.php | 26 ++++++ 11 files changed, 187 insertions(+), 99 deletions(-) create mode 100644 resources/views/accounts/import_ofx.blade.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 97226ad29c2a..3abaaadfd78c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -402,17 +402,9 @@ class AccountController extends BaseController private function showBankAccounts() { - $account = Auth::user()->account; - $account->load('bank_accounts'); - $count = count($account->bank_accounts); - - if ($count == 0) { - return Redirect::to('bank_accounts/create'); - } else { - return View::make('accounts.banks', [ - 'title' => trans('texts.bank_accounts') - ]); - } + return View::make('accounts.banks', [ + 'title' => trans('texts.bank_accounts') + ]); } private function showOnlinePayments() diff --git a/app/Http/Controllers/BankAccountController.php b/app/Http/Controllers/BankAccountController.php index bce86bce42f1..1c003cfd47b0 100644 --- a/app/Http/Controllers/BankAccountController.php +++ b/app/Http/Controllers/BankAccountController.php @@ -13,12 +13,14 @@ use stdClass; use Crypt; use URL; use Utils; +use File; use App\Models\Gateway; use App\Models\Account; use App\Models\BankAccount; use App\Ninja\Repositories\BankAccountRepository; use App\Services\BankAccountService; use App\Http\Requests\CreateBankAccountRequest; +use Illuminate\Http\Request; class BankAccountController extends BaseController { @@ -122,4 +124,28 @@ class BankAccountController extends BaseController return $this->bankAccountService->importExpenses($bankId, Input::all()); } + public function showImportOFX() + { + return view('accounts.import_ofx'); + } + + public function doImportOFX(Request $request) + { + $file = File::get($request->file('ofx_file')); + + try { + $data = $this->bankAccountService->parseOFX($file); + } catch (\Exception $e) { + Session::flash('error', trans('texts.ofx_parse_failed')); + return view('accounts.import_ofx'); + } + + $data = [ + 'banks' => null, + 'bankAccount' => null, + 'transactions' => json_encode([$data]) + ]; + + return View::make('accounts.bank_account', $data); + } } diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 2ce7a633f179..3bb399294c14 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -10,7 +10,7 @@ use Utils; class BaseController extends Controller { use DispatchesJobs, AuthorizesRequests; - + protected $entityType; /** diff --git a/app/Http/routes.php b/app/Http/routes.php index 1d6d6b8fb903..47b1a28b1c4e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -60,7 +60,7 @@ Route::group(['middleware' => 'auth:client'], function() { Route::get('client/documents/js/{documents}/{filename}', 'PublicClientController@getDocumentVFSJS'); Route::get('client/documents/{invitation_key}/{documents}/{filename?}', 'PublicClientController@getDocument'); Route::get('client/documents/{invitation_key}/{filename?}', 'PublicClientController@getInvoiceDocumentsZip'); - + Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable')); Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable')); Route::get('api/client.recurring_invoices', array('as'=>'api.client.recurring_invoices', 'uses'=>'PublicClientController@recurringInvoiceDatatable')); @@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); - + Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); Route::post('users/change_password', 'UserController@changePassword'); @@ -156,7 +156,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS'); Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview'); Route::post('document', 'DocumentController@postUpload'); - + Route::get('quotes/create/{client_id?}', 'QuoteController@create'); Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice'); Route::get('quotes/{invoices}/edit', 'InvoiceController@edit'); @@ -245,6 +245,8 @@ Route::group([ Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); + Route::get('bank_accounts/import_ofx', 'BankAccountController@showImportOFX'); + Route::post('bank_accounts/import_ofx', 'BankAccountController@doImportOFX'); Route::resource('bank_accounts', 'BankAccountController'); Route::get('api/bank_accounts', array('as'=>'api.bank_accounts', 'uses'=>'BankAccountController@getDatatable')); Route::post('bank_accounts/bulk', 'BankAccountController@bulk'); @@ -487,7 +489,7 @@ if (!defined('CONTACT_EMAIL')) { define('INVOICE_STATUS_APPROVED', 4); define('INVOICE_STATUS_PARTIAL', 5); define('INVOICE_STATUS_PAID', 6); - + define('PAYMENT_STATUS_PENDING', 1); define('PAYMENT_STATUS_VOIDED', 2); define('PAYMENT_STATUS_FAILED', 3); @@ -706,7 +708,7 @@ if (!defined('CONTACT_EMAIL')) { define('AUTO_BILL_OPT_IN', 1); define('AUTO_BILL_OPT_OUT', 2); define('AUTO_BILL_ALWAYS', 3); - + // These must be lowercase define('PLAN_FREE', 'free'); define('PLAN_PRO', 'pro'); @@ -714,7 +716,7 @@ if (!defined('CONTACT_EMAIL')) { define('PLAN_WHITE_LABEL', 'white_label'); define('PLAN_TERM_MONTHLY', 'month'); define('PLAN_TERM_YEARLY', 'year'); - + // Pro define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); @@ -729,23 +731,23 @@ if (!defined('CONTACT_EMAIL')) { define('FEATURE_API', 'api'); define('FEATURE_CLIENT_PORTAL_PASSWORD', 'client_portal_password'); define('FEATURE_CUSTOM_URL', 'custom_url'); - + define('FEATURE_MORE_CLIENTS', 'more_clients'); // No trial allowed - + // Whitelabel define('FEATURE_CLIENT_PORTAL_CSS', 'client_portal_css'); define('FEATURE_WHITE_LABEL', 'feature_white_label'); // Enterprise define('FEATURE_DOCUMENTS', 'documents'); - + // No Trial allowed define('FEATURE_USERS', 'users');// Grandfathered for old Pro users define('FEATURE_USER_PERMISSIONS', 'user_permissions'); - + // Pro users who started paying on or before this date will be able to manage users define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-05-15'); - + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -801,4 +803,4 @@ if (Utils::isNinjaDev()) //ini_set('memory_limit','1024M'); //Auth::loginUsingId(1); } -*/ \ No newline at end of file +*/ diff --git a/app/Libraries/OFX.php b/app/Libraries/OFX.php index 721c9f529f85..b32e308a4247 100644 --- a/app/Libraries/OFX.php +++ b/app/Libraries/OFX.php @@ -27,15 +27,15 @@ class OFX curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/x-ofx', 'User-Agent: httpclient')); curl_setopt($c, CURLOPT_POSTFIELDS, $this->request); curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); - + $this->response = curl_exec($c); - + if (Utils::isNinjaDev()) { Log::info(print_r($this->response, true)); } - + curl_close($c); - + $tmp = explode('', $this->response); $this->responseHeader = $tmp[0]; $this->responseBody = ''.$tmp[1]; @@ -48,6 +48,7 @@ class OFX return $x; } + public static function closeTags($x) { $x = preg_replace('/\s+/', '', $x); @@ -233,4 +234,3 @@ class Account } } } - diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 316491a5356b..e626e08549bb 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -90,15 +90,24 @@ class Expense extends EntityModel { return round($this->amount * $this->exchange_rate, 2); } - + public function toArray() { $array = parent::toArray(); - + if(empty($this->visible) || in_array('converted_amount', $this->visible))$array['converted_amount'] = $this->convertedAmount(); - + return $array; } + + public function scopeBankId($query, $bankdId = null) + { + if ($bankdId) { + $query->whereBankId($bankId); + } + + return $query; + } } Expense::creating(function ($expense) { diff --git a/app/Services/BankAccountService.php b/app/Services/BankAccountService.php index 72aada6e3ff9..c04651629856 100644 --- a/app/Services/BankAccountService.php +++ b/app/Services/BankAccountService.php @@ -34,14 +34,10 @@ class BankAccountService extends BaseService return $this->bankAccountRepo; } - public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true) + private function getExpenses($bankId = null) { - if (! $bankId || ! $username || ! $password) { - return false; - } - $expenses = Expense::scope() - ->whereBankId($bankId) + ->bankId($bankId) ->where('transaction_id', '!=', '') ->withTrashed() ->get(['transaction_id']) @@ -50,6 +46,16 @@ class BankAccountService extends BaseService return $val['transaction_id']; }, $expenses)); + return $expenses; + } + + public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true) + { + if (! $bankId || ! $username || ! $password) { + return false; + } + + $expenses = $this->getExpenses(); $vendorMap = $this->createVendorMap(); $bankAccounts = BankSubaccount::scope() ->whereHas('bank_account', function ($query) use ($bankId) { @@ -106,44 +112,60 @@ class BankAccountService extends BaseService $obj->balance = Utils::formatMoney($account->ledgerBalance, CURRENCY_DOLLAR); if ($includeTransactions) { - $ofxParser = new \OfxParser\Parser(); - $ofx = $ofxParser->loadFromString($account->response); - - $obj->start_date = $ofx->BankAccount->Statement->startDate; - $obj->end_date = $ofx->BankAccount->Statement->endDate; - $obj->transactions = []; - - foreach ($ofx->BankAccount->Statement->transactions as $transaction) { - // ensure transactions aren't imported as expenses twice - if (isset($expenses[$transaction->uniqueId])) { - continue; - } - if ($transaction->amount >= 0) { - continue; - } - - // if vendor has already been imported use current name - $vendorName = trim(substr($transaction->name, 0, 20)); - $key = strtolower($vendorName); - $vendor = isset($vendorMap[$key]) ? $vendorMap[$key] : null; - - $transaction->vendor = $vendor ? $vendor->name : $this->prepareValue($vendorName); - $transaction->info = $this->prepareValue(substr($transaction->name, 20)); - $transaction->memo = $this->prepareValue($transaction->memo); - $transaction->date = \Auth::user()->account->formatDate($transaction->date); - $transaction->amount *= -1; - $obj->transactions[] = $transaction; - } + $obj = $this->parseTransactions($obj, $account->response, $expenses, $vendorMap); } return $obj; } + private function parseTransactions($account, $data, $expenses, $vendorMap) + { + $ofxParser = new \OfxParser\Parser(); + $ofx = $ofxParser->loadFromString($data); + + $account->start_date = $ofx->BankAccount->Statement->startDate; + $account->end_date = $ofx->BankAccount->Statement->endDate; + $account->transactions = []; + + foreach ($ofx->BankAccount->Statement->transactions as $transaction) { + // ensure transactions aren't imported as expenses twice + if (isset($expenses[$transaction->uniqueId])) { + continue; + } + if ($transaction->amount >= 0) { + continue; + } + + // if vendor has already been imported use current name + $vendorName = trim(substr($transaction->name, 0, 20)); + $key = strtolower($vendorName); + $vendor = isset($vendorMap[$key]) ? $vendorMap[$key] : null; + + $transaction->vendor = $vendor ? $vendor->name : $this->prepareValue($vendorName); + $transaction->info = $this->prepareValue(substr($transaction->name, 20)); + $transaction->memo = $this->prepareValue($transaction->memo); + $transaction->date = \Auth::user()->account->formatDate($transaction->date); + $transaction->amount *= -1; + $account->transactions[] = $transaction; + } + + return $account; + } + private function prepareValue($value) { return ucwords(strtolower(trim($value))); } + public function parseOFX($data) + { + $account = new stdClass; + $expenses = $this->getExpenses(); + $vendorMap = $this->createVendorMap(); + + return $this->parseTransactions($account, $data, $expenses, $vendorMap); + } + private function createVendorMap() { $vendorMap = []; @@ -158,7 +180,7 @@ class BankAccountService extends BaseService return $vendorMap; } - public function importExpenses($bankId, $input) + public function importExpenses($bankId = 0, $input) { $vendorMap = $this->createVendorMap(); $countVendors = 0; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 5ccec0032495..51f746402a87 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1295,7 +1295,9 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', 'chart_type' => 'Chart Type', 'format' => 'Format', - + 'import_ofx' => 'Import OFX', + 'ofx_file' => 'OFX File', + 'ofx_parse_failed' => 'Failed to parse OFX file', ); return $LANG; diff --git a/resources/views/accounts/bank_account.blade.php b/resources/views/accounts/bank_account.blade.php index ff1a0affef2e..4483bc30252e 100644 --- a/resources/views/accounts/bank_account.blade.php +++ b/resources/views/accounts/bank_account.blade.php @@ -2,7 +2,7 @@ @section('head') @parent - + @include('money_script')