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);
}
/**