diff --git a/app/Console/Commands/CreateTestData.php b/app/Console/Commands/CreateTestData.php index ac7bcf8daa56..0045796945a9 100644 --- a/app/Console/Commands/CreateTestData.php +++ b/app/Console/Commands/CreateTestData.php @@ -286,6 +286,7 @@ class CreateTestData extends Command $company = factory(\App\Models\Company::class)->create([ 'account_id' => $account->id, 'slack_webhook_url' => config('ninja.notification.slack'), + 'is_large' => true, ]); $account->default_company_id = $company->id; diff --git a/app/Console/Commands/DemoMode.php b/app/Console/Commands/DemoMode.php index a2cb0781c43a..92decce9a6ea 100644 --- a/app/Console/Commands/DemoMode.php +++ b/app/Console/Commands/DemoMode.php @@ -101,6 +101,8 @@ class DemoMode extends Command 'account_id' => $account->id, 'slack_webhook_url' => config('ninja.notification.slack'), 'enabled_modules' => 32767, + 'company_key' => 'demo', + 'enable_shop_api' => true ]); $settings = $company->settings; diff --git a/app/DataMapper/EmailTemplateDefaults.php b/app/DataMapper/EmailTemplateDefaults.php index eb831a516f87..e92fda3da951 100644 --- a/app/DataMapper/EmailTemplateDefaults.php +++ b/app/DataMapper/EmailTemplateDefaults.php @@ -121,7 +121,6 @@ class EmailTemplateDefaults return $converter->convertToHtml(self::transformText('invoice_message')); - //return Parsedown::instance()->line(self::transformText('invoice_message')); } public static function emailQuoteSubject() diff --git a/app/Factory/ClientFactory.php b/app/Factory/ClientFactory.php index 9c599ab6e143..7f6460ffb8b2 100644 --- a/app/Factory/ClientFactory.php +++ b/app/Factory/ClientFactory.php @@ -33,8 +33,8 @@ class ClientFactory $client->client_hash = Str::random(40); $client->settings = ClientSettings::defaults(); - $client_contact = ClientContactFactory::create($company_id, $user_id); - $client->contacts->add($client_contact); + // $client_contact = ClientContactFactory::create($company_id, $user_id); + // $client->contacts->add($client_contact); return $client; } diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 35766435dd64..9b6aabd97a29 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -70,9 +70,9 @@ class InvoiceFilters extends QueryFilters return $this->builder; } - public function invoice_number(string $invoice_number):Builder + public function number(string $number) :Builder { - return $this->builder->where('number', $invoice_number); + return $this->builder->where('number', $number); } /** diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index e741f1466da2..ccf9ee51c46b 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -270,7 +270,7 @@ class BaseController extends Controller $query->with($includes); - if (!auth()->user()->hasPermission('view_'.lcfirst(class_basename($this->entity_type)))) { + if (auth()->user() && !auth()->user()->hasPermission('view_'.lcfirst(class_basename($this->entity_type)))) { $query->where('user_id', '=', auth()->user()->id); } @@ -346,7 +346,7 @@ class BaseController extends Controller $data = $this->createItem($item, $transformer, $this->entity_type); - if (request()->include_static) { + if (auth()->user() && request()->include_static) { $data['static'] = Statics::company(auth()->user()->getCompany()->getLocale()); } diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 8453e788f76c..533c419e067a 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -54,7 +54,7 @@ class InvitationController extends Controller event(new InvitationWasViewed($invitation->{$entity}, $invitation, $invitation->{$entity}->company, Ninja::eventVars())); - $this->fireEntityViewedEvent($invitation->{$entity}, $entity); + $this->fireEntityViewedEvent($invitation, $entity); } return redirect()->route('client.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->{$key})]); diff --git a/app/Http/Controllers/Shop/ClientController.php b/app/Http/Controllers/Shop/ClientController.php new file mode 100644 index 000000000000..755615a9e5cb --- /dev/null +++ b/app/Http/Controllers/Shop/ClientController.php @@ -0,0 +1,91 @@ +client_repo = $client_repo; + } + + public function show(Request $request, string $contact_key) + { + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); + + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + + $contact = ClientContact::with('client') + ->where('company_id', $company->id) + ->where('contact_key', $contact_key) + ->firstOrFail(); + + return $this->itemResponse($contact->client); + } + + public function store(StoreShopClientRequest $request) + { + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); + + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + + app('queue')->createPayloadUsing(function () use ($company) { + return ['db' => $company->db]; + }); + + $client = $this->client_repo->save($request->all(), ClientFactory::create($company->id, $company->owner()->id)); + + $client->load('contacts', 'primary_contact'); + + $this->uploadLogo($request->file('company_logo'), $company, $client); + + event(new ClientWasCreated($client, $company, Ninja::eventVars())); + + return $this->itemResponse($client); + } +} diff --git a/app/Http/Controllers/Shop/InvoiceController.php b/app/Http/Controllers/Shop/InvoiceController.php new file mode 100644 index 000000000000..aabd624549fe --- /dev/null +++ b/app/Http/Controllers/Shop/InvoiceController.php @@ -0,0 +1,94 @@ +invoice_repo = $invoice_repo; + } + + public function show(Request $request, string $invitation_key) + { + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); + + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + + $invitation = InvoiceInvitation::with(['invoice']) + ->where('company_id', $company->id) + ->where('key',$invitation_key) + ->firstOrFail(); + + return $this->itemResponse($invitation->invoice); + } + + + public function store(StoreShopInvoiceRequest $request) + { + + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); + + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + + app('queue')->createPayloadUsing(function () use ($company) { + return ['db' => $company->db]; + }); + + $client = Client::find($request->input('client_id')); + + $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($company->id, $company->owner()->id)); + + event(new InvoiceWasCreated($invoice, $company, Ninja::eventVars())); + + $invoice = $invoice->service()->triggeredActions($request)->save(); + + return $this->itemResponse($invoice); + } + +} diff --git a/app/Http/Controllers/Shop/ProductController.php b/app/Http/Controllers/Shop/ProductController.php new file mode 100644 index 000000000000..9489976d6f99 --- /dev/null +++ b/app/Http/Controllers/Shop/ProductController.php @@ -0,0 +1,60 @@ +header('X-API-COMPANY-KEY'))->first(); + + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + + $products = Product::where('company_id', $company->id); + + return $this->listResponse($products); + } + + public function show(Request $request, string $product_key) + { + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); + + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + + $product = Product::where('company_id', $company->id) + ->where('product_key', $product_key) + ->first(); + + return $this->itemResponse($product); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 4799bf0fc7ab..ca3680122a97 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -73,6 +73,11 @@ class Kernel extends HttpKernel \App\Http\Middleware\StartupCheck::class, \App\Http\Middleware\QueryLogging::class, ], + 'shop' => [ + 'throttle:60,1', + 'bindings', + 'query_logging', + ], ]; /** @@ -106,7 +111,10 @@ class Kernel extends HttpKernel 'url_db' => \App\Http\Middleware\UrlSetDb::class, 'web_db' => \App\Http\Middleware\SetWebDb::class, 'api_db' => \App\Http\Middleware\SetDb::class, + 'company_key_db' => \App\Http\Middleware\SetDbByCompanyKey::class, 'locale' => \App\Http\Middleware\Locale::class, 'contact.register' => \App\Http\Middleware\ContactRegister::class, + 'shop_token_auth' => \App\Http\Middleware\Shop\ShopTokenAuth::class, + ]; } diff --git a/app/Http/Middleware/Cors.php b/app/Http/Middleware/Cors.php index 137532e8d8d7..09586788e635 100644 --- a/app/Http/Middleware/Cors.php +++ b/app/Http/Middleware/Cors.php @@ -16,7 +16,7 @@ class Cors // ALLOW OPTIONS METHOD $headers = [ 'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE', - 'Access-Control-Allow-Headers'=> 'X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' + 'Access-Control-Allow-Headers'=> 'X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' ]; return Response::make('OK', 200, $headers); @@ -36,7 +36,7 @@ class Cors $response->headers->set('Access-Control-Allow-Origin', '*'); $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - $response->headers->set('Access-Control-Allow-Headers', 'X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'); + $response->headers->set('Access-Control-Allow-Headers', 'X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'); $response->headers->set('Access-Control-Expose-Headers', 'X-APP-VERSION,X-MINIMUM-CLIENT-VERSION'); $response->headers->set('X-APP-VERSION', config('ninja.app_version')); $response->headers->set('X-MINIMUM-CLIENT-VERSION', config('ninja.minimum_client_version')); diff --git a/app/Http/Middleware/SetDbByCompanyKey.php b/app/Http/Middleware/SetDbByCompanyKey.php new file mode 100644 index 000000000000..e2abfe9bc4e3 --- /dev/null +++ b/app/Http/Middleware/SetDbByCompanyKey.php @@ -0,0 +1,48 @@ + 'Invalid Token', + 'errors' => [] + ]; + + + if ($request->header('X-API-COMPANY-KEY') && config('ninja.db.multi_db_enabled')) { + if (! MultiDB::findAndSetDbByCompanyKey($request->header('X-API-COMPANY-KEY'))) { + return response()->json($error, 403); + } + } elseif (!config('ninja.db.multi_db_enabled')) { + return $next($request); + } else { + return response()->json($error, 403); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/TokenAuth.php b/app/Http/Middleware/TokenAuth.php index b3f840fa6ece..088e51cd43f2 100644 --- a/app/Http/Middleware/TokenAuth.php +++ b/app/Http/Middleware/TokenAuth.php @@ -29,6 +29,7 @@ class TokenAuth public function handle($request, Closure $next) { if ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user','company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first())) { + $user = $company_token->user; $error = [ diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 7dba25743d38..01f2cbedc5dd 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -94,4 +94,11 @@ class UpdateInvoiceRequest extends Request $this->replace($input); } + + public function messages() + { + return [ + 'id' => ctrans('text.locked_invoice'), + ]; + } } diff --git a/app/Http/Requests/Shop/StoreShopClientRequest.php b/app/Http/Requests/Shop/StoreShopClientRequest.php new file mode 100644 index 000000000000..c7ee2497df2f --- /dev/null +++ b/app/Http/Requests/Shop/StoreShopClientRequest.php @@ -0,0 +1,183 @@ +input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.' . $index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + /* Ensure we have a client name, and that all emails are unique*/ + //$rules['name'] = 'required|min:1'; + $rules['id_number'] = 'unique:clients,id_number,' . $this->id . ',id,company_id,' . $this->company_id; + $rules['settings'] = new ValidClientGroupSettingsRule(); + $rules['contacts.*.email'] = 'nullable|distinct'; + $rules['contacts.*.password'] = [ + 'nullable', + 'sometimes', + 'string', + 'min:7', // must be at least 10 characters in length + 'regex:/[a-z]/', // must contain at least one lowercase letter + 'regex:/[A-Z]/', // must contain at least one uppercase letter + 'regex:/[0-9]/', // must contain at least one digit + //'regex:/[@$!%*#?&.]/', // must contain a special character + ]; + + if($this->company->account->isFreeHostedClient()) + $rules['hosted_clients'] = new CanStoreClientsRule($this->company->id); + + return $rules; + } + + + protected function prepareForValidation() + { + $this->company = Company::where('company_key', request()->header('X-API-COMPANY-KEY'))->firstOrFail(); + + $input = $this->all(); + + //@todo implement feature permissions for > 100 clients + // + $settings = ClientSettings::defaults(); + + if (array_key_exists('settings', $input) && !empty($input['settings'])) { + foreach ($input['settings'] as $key => $value) { + $settings->{$key} = $value; + } + } + + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { + $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); + } + + //is no settings->currency_id is set then lets dive in and find either a group or company currency all the below may be redundant!! + if (!property_exists($settings, 'currency_id') && isset($input['group_settings_id'])) { + $input['group_settings_id'] = $this->decodePrimaryKey($input['group_settings_id']); + $group_settings = GroupSetting::find($input['group_settings_id']); + + if ($group_settings && property_exists($group_settings->settings, 'currency_id') && isset($group_settings->settings->currency_id)) { + $settings->currency_id = (string)$group_settings->settings->currency_id; + } else { + $settings->currency_id = (string)$this->company->settings->currency_id; + } + } elseif (!property_exists($settings, 'currency_id')) { + $settings->currency_id = (string)$this->company->settings->currency_id; + } + + if (isset($input['currency_code'])) { + $settings->currency_id = $this->getCurrencyCode($input['currency_code']); + } + + $input['settings'] = $settings; + + if (isset($input['contacts'])) { + foreach ($input['contacts'] as $key => $contact) { + if (array_key_exists('id', $contact) && is_numeric($contact['id'])) { + unset($input['contacts'][$key]['id']); + } elseif (array_key_exists('id', $contact) && is_string($contact['id'])) { + $input['contacts'][$key]['id'] = $this->decodePrimaryKey($contact['id']); + } + + + //Filter the client contact password - if it is sent with ***** we should ignore it! + if (isset($contact['password'])) { + if (strlen($contact['password']) == 0) { + $input['contacts'][$key]['password'] = ''; + } else { + $contact['password'] = str_replace("*", "", $contact['password']); + + if (strlen($contact['password']) == 0) { + unset($input['contacts'][$key]['password']); + } + } + } + } + } + + if(isset($input['country_code'])) { + $input['country_id'] = $this->getCountryCode($input['country_code']); + } + + if(isset($input['shipping_country_code'])) { + $input['shipping_country_id'] = $this->getCountryCode($input['shipping_country_code']); + } + + $this->replace($input); + } + + public function messages() + { + return [ + 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + //'required' => trans('validation.required', ['attribute' => 'email']), + 'contacts.*.email.required' => ctrans('validation.email', ['attribute' => 'email']), + ]; + } + + private function getCountryCode($country_code) + { + $countries = Cache::get('countries'); + + $country = $countries->filter(function ($item) use($country_code) { + return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code; + })->first(); + + return (string) $country->id; + } + + private function getCurrencyCode($code) + { + $currencies = Cache::get('currencies'); + + $currency = $currencies->filter(function ($item) use($code){ + return $item->code == $code; + })->first(); + + return (string) $currency->id; + } + +} diff --git a/app/Http/Requests/Shop/StoreShopInvoiceRequest.php b/app/Http/Requests/Shop/StoreShopInvoiceRequest.php new file mode 100644 index 000000000000..4b895a5e63b4 --- /dev/null +++ b/app/Http/Requests/Shop/StoreShopInvoiceRequest.php @@ -0,0 +1,109 @@ +input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.' . $index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + $rules['client_id'] = 'required|exists:clients,id,company_id,'.$this->company->id; + + $rules['invitations.*.client_contact_id'] = 'distinct'; + + $rules['number'] = new UniqueInvoiceNumberRule($this->all()); + + return $rules; + } + + protected function prepareForValidation() + { + $this->company = Company::where('company_key', request()->header('X-API-COMPANY-KEY'))->firstOrFail(); + + $input = $this->all(); + + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + } + + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { + $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); + } + + if (isset($input['client_contacts'])) { + foreach ($input['client_contacts'] as $key => $contact) { + if (!array_key_exists('send_email', $contact) || !array_key_exists('id', $contact)) { + unset($input['client_contacts'][$key]); + } + } + } + + if (isset($input['invitations'])) { + foreach ($input['invitations'] as $key => $value) { + if (isset($input['invitations'][$key]['id']) && is_numeric($input['invitations'][$key]['id'])) { + unset($input['invitations'][$key]['id']); + } + + if (isset($input['invitations'][$key]['id']) && is_string($input['invitations'][$key]['id'])) { + $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); + } + + if (is_string($input['invitations'][$key]['client_contact_id'])) { + $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); + } + } + } + + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + //$input['line_items'] = json_encode($input['line_items']); + $this->replace($input); + } +} diff --git a/app/Libraries/MultiDB.php b/app/Libraries/MultiDB.php index 457eee507023..d15196a40bce 100644 --- a/app/Libraries/MultiDB.php +++ b/app/Libraries/MultiDB.php @@ -180,6 +180,17 @@ class MultiDB return false; } + public static function findAndSetDbByCompanyKey($company_key) :bool + { + foreach (self::$dbs as $db) { + if ($company = Company::on($db)->where('company_key', $company_key)->first()) { + self::setDb($company->db); + return true; + } + } + return false; + } + public static function findAndSetDbByDomain($subdomain) :bool { foreach (self::$dbs as $db) { diff --git a/app/Listeners/Invoice/InvoiceViewedActivity.php b/app/Listeners/Invoice/InvoiceViewedActivity.php index c3b0e52efecd..72239bf06faf 100644 --- a/app/Listeners/Invoice/InvoiceViewedActivity.php +++ b/app/Listeners/Invoice/InvoiceViewedActivity.php @@ -46,14 +46,14 @@ class InvoiceViewedActivity implements ShouldQueue $fields = new \stdClass; - $fields->user_id = $event->invoice->user_id; - $fields->company_id = $event->invoice->company_id; + $fields->user_id = $event->invitation->user_id; + $fields->company_id = $event->invitation->company_id; $fields->activity_type_id = Activity::VIEW_INVOICE; - $fields->client_id = $event->invitation->client_id; + $fields->client_id = $event->invitation->invoice->client_id; $fields->client_contact_id = $event->invitation->client_contact_id; $fields->invitation_id = $event->invitation->id; $fields->invoice_id = $event->invitation->invoice_id; - $this->activity_repo->save($fields, $event->invoice, $event->event_vars); + $this->activity_repo->save($fields, $event->invitation->invoice, $event->event_vars); } } diff --git a/app/Mail/BouncedEmail.php b/app/Mail/BouncedEmail.php index 7886967cb61b..68876078cc99 100644 --- a/app/Mail/BouncedEmail.php +++ b/app/Mail/BouncedEmail.php @@ -59,7 +59,7 @@ class BouncedEmail extends Mailable implements ShouldQueue //->bcc('') ->queue(new BouncedEmail($invitation)); - return $this->from('turbo124@gmail.com') //todo + return $this->from('x@gmail.com') //todo ->subject(ctrans('texts.confirmation_subject')) ->markdown('email.auth.verify', ['user' => $this->user]) ->text('email.auth.verify_text'); diff --git a/app/Mail/VerifyUser.php b/app/Mail/VerifyUser.php index 13d4587736ef..9ef44712b440 100644 --- a/app/Mail/VerifyUser.php +++ b/app/Mail/VerifyUser.php @@ -39,7 +39,7 @@ class VerifyUser extends Mailable implements ShouldQueue */ public function build() { - return $this->from('turbo124@gmail.com') //todo + return $this->from('x@gmail.com') //todo ->subject(ctrans('texts.confirmation_subject')) ->markdown('email.auth.verify', ['user' => $this->user]) ->text('email.auth.verify_text'); diff --git a/app/Models/Company.php b/app/Models/Company.php index 2dc887c1f1f3..a8c3fa440f10 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -109,6 +109,7 @@ class Company extends BaseModel 'slack_webhook_url', 'google_analytics_key', 'client_can_register', + 'enable_shop_api', ]; diff --git a/app/Models/CompanyUser.php b/app/Models/CompanyUser.php index 5ce07b3efdc7..fa699b40e274 100644 --- a/app/Models/CompanyUser.php +++ b/app/Models/CompanyUser.php @@ -46,6 +46,7 @@ class CompanyUser extends Pivot 'is_owner', 'is_locked', 'slack_webhook_url', + 'shop_restricted' ]; protected $touches = []; diff --git a/app/Notifications/BaseNotification.php b/app/Notifications/BaseNotification.php index 5b9d71b768f8..293cc145c805 100644 --- a/app/Notifications/BaseNotification.php +++ b/app/Notifications/BaseNotification.php @@ -103,7 +103,7 @@ class BaseNotification extends Notification implements ShouldQueue $email_style_custom = $this->settings->email_style_custom; $body = strtr($email_style_custom, "$body", $body); } - + $data = [ 'body' => $body, 'design' => $design_style, @@ -120,4 +120,22 @@ class BaseNotification extends Notification implements ShouldQueue return $data; } + + public function getTemplateView() + { + + switch ($this->settings->email_style) { + case 'plain': + return 'email.template.plain'; + break; + case 'custom': + return 'email.template.custom'; + break; + default: + return 'email.admin.generic_email'; + break; + } + + } + } \ No newline at end of file diff --git a/app/Notifications/SendGenericNotification.php b/app/Notifications/SendGenericNotification.php index fa1395900dde..097047c023cb 100644 --- a/app/Notifications/SendGenericNotification.php +++ b/app/Notifications/SendGenericNotification.php @@ -73,14 +73,17 @@ class SendGenericNotification extends BaseNotification implements ShouldQueue */ public function toMail($notifiable) { + $mail_message = (new MailMessage) ->withSwiftMessage(function ($message) { $message->getHeaders()->addTextHeader('Tag', $this->invitation->company->company_key); - })->markdown('email.admin.generic_email', $this->buildMailMessageData()); + //})->markdown($this->getTemplateView(), $this->buildMailMessageData()); + })->markdown('email.template.plain', $this->buildMailMessageData()); $mail_message = $this->buildMailMessageSettings($mail_message); return $mail_message; + } /** diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 778e1d5ff03a..feddf0b08bc2 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -57,6 +57,8 @@ class RouteServiceProvider extends ServiceProvider $this->mapContactApiRoutes(); $this->mapClientApiRoutes(); + + $this->mapShopApiRoutes(); } /** @@ -117,4 +119,12 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace) ->group(base_path('routes/client.php')); } + + protected function mapShopApiRoutes() + { + Route::prefix('') + ->middleware('shop') + ->namespace($this->namespace) + ->group(base_path('routes/shop.php')); + } } diff --git a/app/Repositories/ClientContactRepository.php b/app/Repositories/ClientContactRepository.php index 0ef048db9468..ac67a055e80c 100644 --- a/app/Repositories/ClientContactRepository.php +++ b/app/Repositories/ClientContactRepository.php @@ -70,6 +70,9 @@ class ClientContactRepository extends BaseRepository }); + //need to reload here to shake off stale contacts + $client->load('contacts'); + //always made sure we have one blank contact to maintain state if ($client->contacts->count() == 0) { diff --git a/app/Repositories/ClientRepository.php b/app/Repositories/ClientRepository.php index 49d46d692024..8b5582cb71fa 100644 --- a/app/Repositories/ClientRepository.php +++ b/app/Repositories/ClientRepository.php @@ -78,7 +78,7 @@ class ClientRepository extends BaseRepository $data['name'] = $client->present()->name(); } - info("{$client->present()->name} has a balance of {$client->balance} with a paid to date of {$client->paid_to_date}"); + //info("{$client->present()->name} has a balance of {$client->balance} with a paid to date of {$client->paid_to_date}"); if (array_key_exists('documents', $data)) { $this->saveDocuments($data['documents'], $client); diff --git a/app/Services/Invoice/MarkPaid.php b/app/Services/Invoice/MarkPaid.php index 76591becad86..be9c38d562a7 100644 --- a/app/Services/Invoice/MarkPaid.php +++ b/app/Services/Invoice/MarkPaid.php @@ -56,6 +56,7 @@ class MarkPaid extends AbstractService $payment->client_id = $this->invoice->client_id; $payment->transaction_reference = ctrans('texts.manual_entry'); $payment->currency_id = $this->invoice->client->getSetting('currency_id'); + $payment->is_manual = true; /* Create a payment relationship to the invoice entity */ $payment->save(); diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 6a31e1d61697..a4709aa50619 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -132,6 +132,7 @@ class CompanyTransformer extends EntityTransformer 'enabled_item_tax_rates' => (int) $company->enabled_item_tax_rates, 'client_can_register' => (bool) $company->client_can_register, 'is_large' => (bool) $company->is_large, + 'enable_shop_api' => (bool) $company->enable_shop_api, ]; } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index fc922bc5f3cd..3a5152d279f3 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -84,22 +84,10 @@ class HtmlEngine - - - - - - - - - - - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private function buildEntityDataArray() :array + public function buildEntityDataArray() :array { if (!$this->client->currency()) { throw new \Exception(debug_backtrace()[1]['function'], 1); @@ -132,21 +120,24 @@ class HtmlEngine $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.invoice_terms')]; $data['$terms'] = &$data['$entity.terms']; - } + $data['$view_link'] = ['value' => ''. ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; + } if ($this->entity_string == 'quote') { $data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.quote')]; $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.quote_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.quote_terms')]; $data['$terms'] = &$data['$entity.terms']; - } + $data['$view_link'] = ['value' => ''. ctrans('texts.view_quote').'', 'label' => ctrans('texts.view_quote')]; + } if ($this->entity_string == 'credit') { $data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.credit')]; $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.credit_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.credit_terms')]; $data['$terms'] = &$data['$entity.terms']; - } + $data['$view_link'] = ['value' => ''. ctrans('texts.view_credit').'', 'label' => ctrans('texts.view_credit')]; + } $data['$entity_number'] = &$data['$number']; diff --git a/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index b752c377647c..6e7eb880b66f 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -60,14 +60,6 @@ class SystemHealth $system_health = false; } - if (!self::checkNode()) { - $system_health = false; - } - - if (!self::checkNpm()) { - $system_health = false; - } - return [ 'system_health' => $system_health, 'extensions' => self::extensions(), @@ -90,13 +82,14 @@ class SystemHealth exec('node -v', $foo, $exitCode); if ($exitCode === 0) { - return true; + return $foo[0]; } - - return false; + } catch (\Exception $e) { - return false; + + return false; } + } public static function checkNpm() @@ -105,14 +98,14 @@ class SystemHealth exec('npm -v', $foo, $exitCode); if ($exitCode === 0) { - return true; - } + return $foo[0]; + } - return false; - - } catch (\Exception $e) { - return false; + }catch (\Exception $e) { + + return false; } + } private static function simpleDbCheck() :bool diff --git a/app/Utils/Traits/MakesInvoiceValues.php b/app/Utils/Traits/MakesInvoiceValues.php index 626241f57894..e5d573c54491 100644 --- a/app/Utils/Traits/MakesInvoiceValues.php +++ b/app/Utils/Traits/MakesInvoiceValues.php @@ -187,6 +187,7 @@ trait MakesInvoiceValues } $calc = $this->calc(); + $invitation = $this->invitations->where('client_contact_id', $contact->id)->first(); $data = []; $data['$tax'] = ['value' => '', 'label' => ctrans('texts.tax')]; @@ -214,6 +215,7 @@ trait MakesInvoiceValues $data['$number'] = ['value' => $this->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$entity.terms'] = ['value' => $this->terms ?: ' ', 'label' => ctrans('texts.invoice_terms')]; $data['$terms'] = &$data['$entity.terms']; + $data['$view_link'] = ['value' => ''. ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; } if ($this instanceof Quote) { @@ -221,13 +223,15 @@ trait MakesInvoiceValues $data['$number'] = ['value' => $this->number ?: ' ', 'label' => ctrans('texts.quote_number')]; $data['$entity.terms'] = ['value' => $this->terms ?: ' ', 'label' => ctrans('texts.quote_terms')]; $data['$terms'] = &$data['$entity.terms']; - } + $data['$view_link'] = ['value' => ''. ctrans('texts.view_quote').'', 'label' => ctrans('texts.view_quote')]; + } if ($this instanceof Credit) { $data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.credit')]; $data['$number'] = ['value' => $this->number ?: ' ', 'label' => ctrans('texts.credit_number')]; $data['$entity.terms'] = ['value' => $this->terms ?: ' ', 'label' => ctrans('texts.credit_terms')]; $data['$terms'] = &$data['$entity.terms']; + $data['$view_link'] = ['value' => ''. ctrans('texts.view_credit').'', 'label' => ctrans('texts.view_credit')]; } $data['$entity_number'] = &$data['$number']; diff --git a/config/auth.php b/config/auth.php index 75a46ab80568..dc92eeb0c348 100644 --- a/config/auth.php +++ b/config/auth.php @@ -78,7 +78,6 @@ return [ ], 'contacts' => [ 'driver' => 'eloquent', - 'model' => App\Models\ClientContact::class, ], diff --git a/database/migrations/2020_07_28_104218_shop_token.php b/database/migrations/2020_07_28_104218_shop_token.php new file mode 100644 index 000000000000..7996ffa0a8b9 --- /dev/null +++ b/database/migrations/2020_07_28_104218_shop_token.php @@ -0,0 +1,30 @@ +boolean('enable_shop_api')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/routes/api.php b/routes/api.php index e1feb2bdeea4..1db2f96a6b8d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -136,8 +136,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('emails', 'EmailController@send')->name('email.send'); /*Subscription and Webhook routes */ - Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe'); - Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe'); + // Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe'); + // Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe'); Route::resource('webhooks', 'WebhookController'); Route::post('webhooks/bulk', 'WebhookController@bulk')->name('webhooks.bulk'); diff --git a/routes/shop.php b/routes/shop.php new file mode 100644 index 000000000000..9187fdf2be8a --- /dev/null +++ b/routes/shop.php @@ -0,0 +1,14 @@ + ['company_key_db','locale'], 'prefix' => 'api/v1'], function () { + + Route::get('shop/products', 'Shop\ProductController@index'); + Route::post('shop/clients', 'Shop\ClientController@store'); + Route::post('shop/invoices', 'Shop\InvoiceController@store'); + Route::get('shop/client/{contact_key}', 'Shop\ClientController@show'); + Route::get('shop/invoice/{invitation_key}', 'Shop\InvoiceController@show'); + Route::get('shop/product/{product_key}', 'Shop\ProductController@show'); + +}); \ No newline at end of file diff --git a/tests/Feature/Shop/ShopInvoiceTest.php b/tests/Feature/Shop/ShopInvoiceTest.php new file mode 100644 index 000000000000..e661c9e35872 --- /dev/null +++ b/tests/Feature/Shop/ShopInvoiceTest.php @@ -0,0 +1,204 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + + public function testTokenSuccess() + { + $this->company->enable_shop_api = true; + $this->company->save(); + + $response = null; + + try { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->get('api/v1/shop/products'); + } + + catch (ValidationException $e) { + $this->assertNotNull($message); + } + + $response->assertStatus(200); + } + + public function testTokenFailure() + { + + $this->company->enable_shop_api = true; + $this->company->save(); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->get('/api/v1/products'); + + + $response->assertStatus(403); + + $arr = $response->json(); + } + + public function testCompanyEnableShopApiBooleanWorks() + { + try { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->get('api/v1/shop/products'); + } + + catch (ValidationException $e) { + $this->assertNotNull($message); + } + + $response->assertStatus(403); + } + + public function testGetByProductKey() + { + + $this->company->enable_shop_api = true; + $this->company->save(); + + $product = factory(\App\Models\Product::class)->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->get('/api/v1/shop/product/'.$product->product_key); + + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals($product->hashed_id, $arr['data']['id']); + } + + public function testGetByClientByContactKey() + { + + $this->company->enable_shop_api = true; + $this->company->save(); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->get('/api/v1/shop/client/'.$this->client->contacts->first()->contact_key); + + + $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals($this->client->hashed_id, $arr['data']['id']); + + } + + public function testCreateClientOnShopRoute() + { + + $this->company->enable_shop_api = true; + $this->company->save(); + + + $data = [ + 'name' => 'ShopClient', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->post('/api/v1/shop/clients/', $data); + + + $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals('ShopClient', $arr['data']['name']); + + } + + public function testCreateInvoiceOnShopRoute() + { + + $this->company->enable_shop_api = true; + $this->company->save(); + + $data = [ + 'name' => 'ShopClient', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->post('/api/v1/shop/clients/', $data); + + + $response->assertStatus(200); + $arr = $response->json(); + + $client_hashed_id = $arr['data']['id']; + + $invoice_data = [ + 'client_id' => $client_hashed_id, + 'po_number' => 'shop_order' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->post('/api/v1/shop/invoices/', $invoice_data); + + + $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals('shop_order', $arr['data']['po_number']); + + + } + +} diff --git a/tests/Integration/DesignTest.php b/tests/Integration/DesignTest.php index b311d5a1a609..1a4fdd71d65d 100644 --- a/tests/Integration/DesignTest.php +++ b/tests/Integration/DesignTest.php @@ -92,7 +92,6 @@ class DesignTest extends TestCase $this->assertNotNull($html); - $this->quote = factory(\App\Models\Invoice::class)->create([ 'user_id' => $this->user->id, 'client_id' => $this->client->id, diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 18c02ecc300d..35096ddc2f2a 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -225,6 +225,7 @@ trait MockAccountData $this->quote = $this->quote_calc->getQuote(); $this->quote->number = $this->getNextQuoteNumber($this->client); + $this->quote->service()->createInvitations()->markSent(); $this->quote->setRelation('client', $this->client); $this->quote->setRelation('company', $this->company); @@ -242,6 +243,7 @@ trait MockAccountData $this->credit->save(); + $this->credit->service()->createInvitations()->markSent(); $this->credit_calc = new InvoiceSum($this->credit); $this->credit_calc->build(); diff --git a/tests/Unit/FactoryCreationTest.php b/tests/Unit/FactoryCreationTest.php index 32eaf760b6fa..47c947da53a9 100644 --- a/tests/Unit/FactoryCreationTest.php +++ b/tests/Unit/FactoryCreationTest.php @@ -126,8 +126,8 @@ class FactoryCreationTest extends TestCase $cliz->save(); $this->assertNotNull($cliz->contacts); - $this->assertEquals(1, $cliz->contacts->count()); - $this->assertInternalType("int", $cliz->contacts->first()->id); + $this->assertEquals(0, $cliz->contacts->count()); + $this->assertInternalType("int", $cliz->id); } /**