1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-21 00:41:34 +02:00
This commit is contained in:
David Bomba 2020-07-30 07:48:33 +10:00
commit 68e2e2c8f4
41 changed files with 948 additions and 60 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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()

View File

@ -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;
}

View File

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

View File

@ -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());
}

View File

@ -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})]);

View File

@ -0,0 +1,91 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers\Shop;
use App\Events\Client\ClientWasCreated;
use App\Factory\ClientFactory;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Client\StoreClientRequest;
use App\Http\Requests\Shop\StoreShopClientRequest;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Repositories\ClientRepository;
use App\Transformers\ClientTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Uploadable;
use Illuminate\Http\Request;
class ClientController extends BaseController
{
use MakesHash;
use Uploadable;
protected $entity_type = Client::class;
protected $entity_transformer = ClientTransformer::class;
/**
* @var ClientRepository
*/
protected $client_repo;
/**
* ClientController constructor.
* @param ClientRepository $clientRepo
*/
public function __construct(ClientRepository $client_repo)
{
parent::__construct();
$this->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);
}
}

View File

@ -0,0 +1,94 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers\Shop;
use App\Events\Invoice\InvoiceWasCreated;
use App\Factory\InvoiceFactory;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Shop\StoreShopInvoiceRequest;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Repositories\InvoiceRepository;
use App\Transformers\InvoiceTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
class InvoiceController extends BaseController
{
use MakesHash;
protected $entity_type = Invoice::class;
protected $entity_transformer = InvoiceTransformer::class;
/**
* @var InvoiceRepository
*/
protected $invoice_repo;
/**
* InvoiceController constructor.
*
* @param \App\Repositories\InvoiceRepository $invoice_repo The invoice repo
*/
public function __construct(InvoiceRepository $invoice_repo)
{
parent::__construct();
$this->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);
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\BaseController;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\Product;
use App\Transformers\ProductTransformer;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
class ProductController extends BaseController
{
use MakesHash;
protected $entity_type = Product::class;
protected $entity_transformer = ProductTransformer::class;
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $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);
$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);
}
}

View File

@ -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,
];
}

View File

@ -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'));

View File

@ -0,0 +1,48 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Middleware;
use App\Libraries\MultiDB;
use App\Models\CompanyToken;
use Closure;
class SetDbByCompanyKey
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$error = [
'message' => '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);
}
}

View File

@ -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 = [

View File

@ -94,4 +94,11 @@ class UpdateInvoiceRequest extends Request
$this->replace($input);
}
public function messages()
{
return [
'id' => ctrans('text.locked_invoice'),
];
}
}

View File

@ -0,0 +1,183 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Shop;
use App\DataMapper\ClientSettings;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Ninja\CanStoreClientsRule;
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
use App\Models\Client;
use App\Models\Company;
use App\Models\GroupSetting;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule;
class StoreShopClientRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
private $company;
public function authorize() : bool
{
return true;
}
public function rules()
{
if ($this->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;
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Shop;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Invoice\UniqueInvoiceNumberRule;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Invoice;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
class StoreShopInvoiceRequest extends Request
{
use MakesHash;
use CleanLineItems;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
private $company;
public function authorize() : bool
{
return true;
}
public function rules()
{
$rules = [];
if ($this->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);
}
}

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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');

View File

@ -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');

View File

@ -109,6 +109,7 @@ class Company extends BaseModel
'slack_webhook_url',
'google_analytics_key',
'client_can_register',
'enable_shop_api',
];

View File

@ -46,6 +46,7 @@ class CompanyUser extends Pivot
'is_owner',
'is_locked',
'slack_webhook_url',
'shop_restricted'
];
protected $touches = [];

View File

@ -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;
}
}
}

View File

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

View File

@ -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'));
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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();

View File

@ -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,
];
}

View File

@ -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 ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.invoice_terms')];
$data['$terms'] = &$data['$entity.terms'];
}
$data['$view_link'] = ['value' => '<a href="' .$this->invitation->getLink() .'">'. ctrans('texts.view_invoice').'</a>', 'label' => ctrans('texts.view_invoice')];
}
if ($this->entity_string == 'quote') {
$data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.quote')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.quote_terms')];
$data['$terms'] = &$data['$entity.terms'];
}
$data['$view_link'] = ['value' => '<a href="' .$this->invitation->getLink() .'">'. ctrans('texts.view_quote').'</a>', 'label' => ctrans('texts.view_quote')];
}
if ($this->entity_string == 'credit') {
$data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.credit')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.credit_terms')];
$data['$terms'] = &$data['$entity.terms'];
}
$data['$view_link'] = ['value' => '<a href="' .$this->invitation->getLink() .'">'. ctrans('texts.view_credit').'</a>', 'label' => ctrans('texts.view_credit')];
}
$data['$entity_number'] = &$data['$number'];

View File

@ -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

View File

@ -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 ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$entity.terms'] = ['value' => $this->terms ?: '&nbsp;', 'label' => ctrans('texts.invoice_terms')];
$data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a href="' .$invitation->getLink() .'">'. ctrans('texts.view_invoice').'</a>', 'label' => ctrans('texts.view_invoice')];
}
if ($this instanceof Quote) {
@ -221,13 +223,15 @@ trait MakesInvoiceValues
$data['$number'] = ['value' => $this->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')];
$data['$entity.terms'] = ['value' => $this->terms ?: '&nbsp;', 'label' => ctrans('texts.quote_terms')];
$data['$terms'] = &$data['$entity.terms'];
}
$data['$view_link'] = ['value' => '<a href="' .$invitation->getLink() .'">'. ctrans('texts.view_quote').'</a>', 'label' => ctrans('texts.view_quote')];
}
if ($this instanceof Credit) {
$data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.credit')];
$data['$number'] = ['value' => $this->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')];
$data['$entity.terms'] = ['value' => $this->terms ?: '&nbsp;', 'label' => ctrans('texts.credit_terms')];
$data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a href="' .$invitation->getLink() .'">'. ctrans('texts.view_credit').'</a>', 'label' => ctrans('texts.view_credit')];
}
$data['$entity_number'] = &$data['$number'];

View File

@ -78,7 +78,6 @@ return [
],
'contacts' => [
'driver' => 'eloquent',
'model' => App\Models\ClientContact::class,
],

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ShopToken extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('companies', function (Blueprint $table) {
$table->boolean('enable_shop_api')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -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');

14
routes/shop.php Normal file
View File

@ -0,0 +1,14 @@
<?php
use Illuminate\Support\Facades\Route;
Route::group(['middleware' => ['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');
});

View File

@ -0,0 +1,204 @@
<?php
namespace Tests\Feature\Shop;
use App\Factory\CompanyUserFactory;
use App\Models\CompanyToken;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\Shop\InvoiceController
*/
class ShopInvoiceTest extends TestCase
{
use MakesHash;
use MockAccountData;
public function setUp() :void
{
parent::setUp();
$this->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']);
}
}

View File

@ -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,

View File

@ -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();

View File

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