Merge branch 'release-3.2.0'

This commit is contained in:
Hillel Coren 2017-04-02 23:16:12 +03:00
commit 9fff9fb2af
270 changed files with 9386 additions and 3565 deletions

View File

@ -3,6 +3,7 @@ APP_DEBUG=false
APP_URL=http://ninja.dev
APP_KEY=SomeRandomStringSomeRandomString
APP_CIPHER=AES-256-CBC
APP_LOCALE=en
DB_TYPE=mysql
DB_STRICT=false
@ -90,7 +91,8 @@ WEPAY_ENVIRONMENT=production # production or stage
WEPAY_AUTO_UPDATE=true # Requires permission from WePay
WEPAY_ENABLE_CANADA=true
WEPAY_FEE_PAYER=payee
WEPAY_APP_FEE_MULTIPLIER=0.002
WEPAY_APP_FEE_CC_MULTIPLIER=0
WEPAY_APP_FEE_ACH_MULTIPLIER=0
WEPAY_APP_FEE_FIXED=0
WEPAY_THEME='{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}' # See https://www.wepay.com/developer/reference/structures#theme

View File

@ -50,6 +50,7 @@ before_script:
- sed -i 's/APP_ENV=production/APP_ENV=development/g' .env
- sed -i 's/APP_DEBUG=false/APP_DEBUG=true/g' .env
- sed -i 's/MAIL_DRIVER=smtp/MAIL_DRIVER=log/g' .env
- sed -i 's/PHANTOMJS_CLOUD_KEY/#PHANTOMJS_CLOUD_KEY/g' .env
- sed -i '$a NINJA_DEV=true' .env
- sed -i '$a TRAVIS=true' .env
# create the database and user
@ -58,7 +59,6 @@ before_script:
# migrate and seed the database
- php artisan migrate --no-interaction
- php artisan db:seed --no-interaction # default seed
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed
# Start webserver on ninja.dev:8000
- php artisan serve --host=ninja.dev --port=8000 & # '&' allows to run in background
# Start PhantomJS
@ -67,10 +67,10 @@ before_script:
- sleep 5
# Make sure the app is up-to-date
- curl -L http://ninja.dev:8000/update
#- php artisan ninja:create-test-data 25
- php artisan ninja:create-test-data 4 true
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed
script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance CheckBalanceCest.php
@ -83,23 +83,29 @@ script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance OnlinePaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance PaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaskCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance GatewayFeesCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php
#- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env
#- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php
after_script:
- php artisan ninja:check-data --no-interaction
- cat .env
- mysql -u root -e 'select * from accounts;' ninja
- mysql -u root -e 'select * from users;' ninja
- mysql -u root -e 'select * from account_gateways;' ninja
- mysql -u root -e 'select * from clients;' ninja
- mysql -u root -e 'select * from contacts;' ninja
- mysql -u root -e 'select * from invoices;' ninja
- mysql -u root -e 'select * from invoice_items;' ninja
- mysql -u root -e 'select * from invitations;' ninja
- mysql -u root -e 'select * from payments;' ninja
- mysql -u root -e 'select * from credits;' ninja
- mysql -u root -e 'select * from expenses;' ninja
- cat storage/logs/laravel-error.log
- cat storage/logs/laravel-info.log
- FILES=$(find tests/_output -type f -name '*.png')
- FILES=$(find tests/_output -type f -name '*.png' | sort -nr)
- for i in $FILES; do echo $i; base64 "$i"; break; done
notifications:

View File

@ -4,6 +4,8 @@ Thanks for your contributions!
## Submit bug reports or feature requests
Please discuss the changes with us ahead of time to ensure they will be merged.
### Submit pull requests
* [Fork](https://github.com/invoiceninja/invoiceninja#fork-destination-box) the [Invoice Ninja repository](https://github.com/invoiceninja/invoiceninja)
* Create a new branch with the name `#issue_number-Short-description`
@ -11,7 +13,7 @@ Thanks for your contributions!
* Make your changes and commit
* Check if your branch is still in sync with the repositorys **`develop`** branch
* _Read:_ [Syncing a fork](https://help.github.com/articles/syncing-a-fork/)
* _Also read:_ [How to rebase a pull request](https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request)
* _Also read:_ [How to rebase a pull request](https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request)
* Push your branch and create a PR against the Invoice Ninja **`develop`** branch
* Update the [Changelog](CHANGELOG.md)
@ -21,7 +23,7 @@ To make the contribution process nice and easy for anyone, please follow some ru
to give a more detailed explanation.
* Only one feature/bugfix per issue. If you want to submit more, create multiple issues.
* Only one feature/bugfix per PR(pull request). Split more changes into multiple PRs.
#### Coding Style
Try to follow the [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
@ -29,7 +31,7 @@ _Example styling:_
```php
/**
* Gets a preview of the email
*
*
* @param TemplateService $templateService
*
* @return \Illuminate\Http\Response

View File

@ -4,6 +4,7 @@ namespace App\Console\Commands;
use Carbon;
use DB;
use Exception;
use Illuminate\Console\Command;
use Mail;
use Symfony\Component\Console\Input\InputOption;
@ -83,6 +84,8 @@ class CheckData extends Command
->from(CONTACT_EMAIL)
->subject('Check-Data: ' . strtoupper($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
});
} elseif (! $this->isValid) {
throw new Exception('Check data failed!!');
}
}
@ -157,9 +160,15 @@ class CheckData extends Command
'products' => [
ENTITY_USER,
],
'vendors' => [
ENTITY_USER,
],
'expense_categories' => [
ENTITY_USER,
],
'payment_terms' => [
ENTITY_USER,
],
'projects' => [
ENTITY_USER,
ENTITY_CLIENT,

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Repositories\ClientRepository;
use App\Ninja\Repositories\ExpenseRepository;
use App\Ninja\Repositories\InvoiceRepository;
@ -25,7 +26,7 @@ class CreateTestData extends Command
/**
* @var string
*/
protected $signature = 'ninja:create-test-data {count=1}';
protected $signature = 'ninja:create-test-data {count=1} {create_account=false}';
/**
* @var
@ -40,13 +41,15 @@ class CreateTestData extends Command
* @param PaymentRepository $paymentRepo
* @param VendorRepository $vendorRepo
* @param ExpenseRepository $expenseRepo
* @param AccountRepository $accountRepo
*/
public function __construct(
ClientRepository $clientRepo,
InvoiceRepository $invoiceRepo,
PaymentRepository $paymentRepo,
VendorRepository $vendorRepo,
ExpenseRepository $expenseRepo)
ExpenseRepository $expenseRepo,
AccountRepository $accountRepo)
{
parent::__construct();
@ -57,6 +60,7 @@ class CreateTestData extends Command
$this->paymentRepo = $paymentRepo;
$this->vendorRepo = $vendorRepo;
$this->expenseRepo = $expenseRepo;
$this->accountRepo = $accountRepo;
}
/**
@ -69,10 +73,21 @@ class CreateTestData extends Command
}
$this->info(date('Y-m-d').' Running CreateTestData...');
Auth::loginUsingId(1);
$this->count = $this->argument('count');
if (filter_var($this->argument('create_account'), FILTER_VALIDATE_BOOLEAN)) {
$this->info('Creating new account...');
$account = $this->accountRepo->create(
$this->faker->firstName,
$this->faker->lastName,
$this->faker->safeEmail
);
Auth::login($account->users[0]);
} else {
$this->info('Using first account...');
Auth::loginUsingId(1);
}
$this->createClients();
$this->createVendors();
@ -182,7 +197,7 @@ class CreateTestData extends Command
'vendor_id' => $vendor->id,
'amount' => $this->faker->randomFloat(2, 1, 10),
'expense_date' => null,
'public_notes' => null,
'public_notes' => '',
];
$expense = $this->expenseRepo->save($data);

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Invoice;
use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\InvoiceRepository;
@ -57,9 +58,18 @@ class SendRecurringInvoices extends Command
public function fire()
{
$this->info(date('Y-m-d').' Running SendRecurringInvoices...');
$this->info(date('Y-m-d H:i:s') . ' Running SendRecurringInvoices...');
$today = new DateTime();
// check for counter resets
$accounts = Account::where('reset_counter_frequency_id', '>', 0)
->orderBy('id', 'asc')
->get();
foreach ($accounts as $account) {
$account->checkCounterReset();
}
$invoices = Invoice::with('account.timezone', 'invoice_items', 'client', 'user')
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND is_public IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today])
->orderBy('id', 'asc')
@ -74,7 +84,8 @@ class SendRecurringInvoices extends Command
continue;
}
$recurInvoice->account->loadLocalizationSettings($recurInvoice->client);
$account = $recurInvoice->account;
$account->loadLocalizationSettings($recurInvoice->client);
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
if ($invoice && ! $invoice->isPaid()) {
@ -103,7 +114,7 @@ class SendRecurringInvoices extends Command
}
}
$this->info('Done');
$this->info(date('Y-m-d H:i:s') . ' Done');
}
/**

View File

@ -23,11 +23,12 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/$LOWER_NAME$",
* summary="List of $LOWER_NAME$",
* summary="List $LOWER_NAME$",
* operationId="list$STUDLY_NAME$s",
* tags={"$LOWER_NAME$"},
* @SWG\Response(
* response=200,
* description="A list with $LOWER_NAME$",
* description="A list of $LOWER_NAME$",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ),
* @SWG\Response(
@ -47,7 +48,14 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
* @SWG\Get(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* summary="Individual $STUDLY_NAME$",
* operationId="get$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="path",
* name="$LOWER_NAME$_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single $LOWER_NAME$",
@ -59,7 +67,6 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
* )
* )
*/
public function show($STUDLY_NAME$Request $request)
{
return $this->itemResponse($request->entity());
@ -71,11 +78,12 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
/**
* @SWG\Post(
* path="/$LOWER_NAME$",
* tags={"$LOWER_NAME$"},
* summary="Create a $LOWER_NAME$",
* operationId="create$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="$LOWER_NAME$",
* @SWG\Schema(ref="#/definitions/$STUDLY_NAME$")
* ),
* @SWG\Response(
@ -99,16 +107,23 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
/**
* @SWG\Put(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* tags={"$LOWER_NAME$"},
* summary="Update a $LOWER_NAME$",
* operationId="update$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="path",
* name="$LOWER_NAME$_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="$LOWER_NAME$",
* @SWG\Schema(ref="#/definitions/$STUDLY_NAME$")
* ),
* @SWG\Response(
* response=200,
* description="Update $LOWER_NAME$",
* description="Updated $LOWER_NAME$",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ),
* @SWG\Response(
@ -117,7 +132,6 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
* )
* )
*/
public function update(Update$STUDLY_NAME$Request $request, $publicId)
{
if ($request->action) {
@ -133,16 +147,18 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
/**
* @SWG\Delete(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* tags={"$LOWER_NAME$"},
* summary="Delete a $LOWER_NAME$",
* operationId="delete$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="body",
* name="body",
* @SWG\Schema(ref="#/definitions/$STUDLY_NAME$")
* in="path",
* name="$LOWER_NAME$_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Delete $LOWER_NAME$",
* description="Deleted $LOWER_NAME$",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ),
* @SWG\Response(
@ -151,7 +167,6 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
* )
* )
*/
public function destroy(Update$STUDLY_NAME$Request $request)
{
$$LOWER_NAME$ = $request->entity();

View File

@ -15,8 +15,8 @@ class $STUDLY_NAME$Transformer extends EntityTransformer
* @SWG\Property(property="id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="user_id", type="integer", example=1)
* @SWG\Property(property="account_key", type="string", example="123456")
* @SWG\Property(property="updated_at", type="timestamp", example="")
* @SWG\Property(property="archived_at", type="timestamp", example="1451160233")
* @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="archived_at", type="integer", example=1451160233, readOnly=true)
*/
/**

View File

@ -41,6 +41,11 @@ if (! defined('APP_NAME')) {
define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2);
define('INVOICE_ITEM_TYPE_STANDARD', 1);
define('INVOICE_ITEM_TYPE_TASK', 2);
define('INVOICE_ITEM_TYPE_PENDING_GATEWAY_FEE', 3);
define('INVOICE_ITEM_TYPE_PAID_GATEWAY_FEE', 4);
define('PERSON_CONTACT', 'contact');
define('PERSON_USER', 'user');
define('PERSON_VENDOR_CONTACT', 'vendorcontact');
@ -283,7 +288,6 @@ if (! defined('APP_NAME')) {
define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN');
define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID');
define('PREV_USER_ID', 'PREV_USER_ID');
define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h');
define('NINJA_LICENSE_ACCOUNT_KEY', 'AsFmBAeLXF0IKf7tmi0eiyZfmWW9hxMT');
define('NINJA_GATEWAY_ID', GATEWAY_STRIPE);
@ -292,7 +296,7 @@ if (! defined('APP_NAME')) {
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '3.1.3' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '3.2.0' . env('NINJA_VERSION_SUFFIX'));
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));

View File

@ -10,6 +10,7 @@ use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Foundation\Validation\ValidationException;
use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Support\Facades\Response;
use Illuminate\Session\TokenMismatchException;
use Redirect;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -26,10 +27,11 @@ class Handler extends ExceptionHandler
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
TokenMismatchException::class,
ModelNotFoundException::class,
ValidationException::class,
//AuthorizationException::class,
//HttpException::class,
//ValidationException::class,
];
/**
@ -43,11 +45,20 @@ class Handler extends ExceptionHandler
*/
public function report(Exception $e)
{
if (! $this->shouldReport($e)) {
return false;
}
// don't show these errors in the logs
if ($e instanceof NotFoundHttpException) {
if (Crawler::isCrawler()) {
return false;
}
// The logo can take a few seconds to get synced between servers
// TODO: remove once we're using cloud storage for logos
if (Utils::isNinja() && strpos(request()->url(), '/logo/') !== false) {
return false;
}
} elseif ($e instanceof HttpResponseException) {
return false;
}
@ -74,9 +85,9 @@ class Handler extends ExceptionHandler
if ($e instanceof ModelNotFoundException) {
return Redirect::to('/');
}
if ($e instanceof \Illuminate\Session\TokenMismatchException) {
// prevent loop since the page auto-submits
if ($request->path() != 'get_started') {
if ($e instanceof TokenMismatchException) {
if (! in_array($request->path(), ['get_started', 'save_sidebar_state'])) {
// https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e
return redirect()
->back()

View File

@ -6,6 +6,7 @@ use App\Events\UserSignedUp;
use App\Http\Requests\RegisterRequest;
use App\Http\Requests\UpdateAccountRequest;
use App\Models\Account;
use App\Ninja\OAuth\OAuth;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Transformers\AccountTransformer;
use App\Ninja\Transformers\UserAccountTransformer;
@ -121,6 +122,7 @@ class AccountApiController extends BaseAPIController
for ($x = 0; $x < count($devices); $x++) {
if ($devices[$x]['email'] == Auth::user()->username) {
$devices[$x]['token'] = $request->token; //update
$devices[$x]['device'] = $request->device;
$account->devices = json_encode($devices);
$account->save();
$devices[$x]['account_key'] = $account->account_key;
@ -187,25 +189,15 @@ class AccountApiController extends BaseAPIController
$token = $request->input('token');
$provider = $request->input('provider');
try {
$user = Socialite::driver($provider)->stateless()->userFromToken($token);
} catch (Exception $exception) {
return $this->errorResponse(['message' => $exception->getMessage()], 401);
}
$oAuth = new OAuth();
$user = $oAuth->getProvider($provider)->getTokenResponse($token);
if ($user) {
$providerId = AuthService::getProviderId($provider);
$user = $this->accountRepo->findUserByOauth($providerId, $user->id);
}
if ($user) {
if($user) {
Auth::login($user);
return $this->processLogin($request);
} else {
sleep(ERROR_DELAY);
return $this->errorResponse(['message' => 'Invalid credentials'], 401);
}
else
return $this->errorResponse(['message' => 'Invalid credentials'], 401);
}
}

View File

@ -9,7 +9,6 @@ use App\Http\Requests\SaveEmailSettings;
use App\Http\Requests\UpdateAccountRequest;
use App\Models\Account;
use App\Models\AccountGateway;
use App\Models\AccountGatewaySettings;
use App\Models\Affiliate;
use App\Models\Document;
use App\Models\Gateway;
@ -38,6 +37,7 @@ use Request;
use Response;
use Session;
use stdClass;
use Exception;
use URL;
use Utils;
@ -123,17 +123,16 @@ class AccountController extends BaseController
{
$user = false;
$guestKey = Input::get('guest_key'); // local storage key to login until registered
$prevUserId = Session::pull(PREV_USER_ID); // last user id used to link to new account
if (Auth::check()) {
return Redirect::to('invoices/create');
}
if (! Utils::isNinja() && (Account::count() > 0 && ! $prevUserId)) {
if (! Utils::isNinja() && Account::count() > 0) {
return Redirect::to('/login');
}
if ($guestKey && ! $prevUserId) {
if ($guestKey) {
$user = User::where('password', '=', $guestKey)->first();
if ($user && $user->registered) {
@ -144,11 +143,6 @@ class AccountController extends BaseController
if (! $user) {
$account = $this->accountRepo->create();
$user = $account->users()->first();
if ($prevUserId) {
$users = $this->accountRepo->associateAccounts($user->id, $prevUserId);
Session::put(SESSION_USER_ACCOUNTS, $users);
}
}
Auth::login($user, true);
@ -186,22 +180,8 @@ class AccountController extends BaseController
$newPlan['price'] = Utils::getPlanPrice($newPlan);
$credit = 0;
if (! empty($planDetails['started']) && $plan == PLAN_FREE) {
// Downgrade
$refund_deadline = clone $planDetails['started'];
$refund_deadline->modify('+30 days');
if ($plan == PLAN_FREE && $refund_deadline >= date_create()) {
if ($payment = $account->company->payment) {
$ninjaAccount = $this->accountRepo->getNinjaAccount();
$paymentDriver = $ninjaAccount->paymentDriver();
$paymentDriver->refundPayment($payment);
Session::flash('message', trans('texts.plan_refunded'));
\Log::info("Refunded Plan Payment: {$account->name} - {$user->email} - Deadline: {$refund_deadline->format('Y-m-d')}");
} else {
Session::flash('message', trans('texts.updated_plan'));
}
}
if ($plan == PLAN_FREE && $company->processRefund(Auth::user())) {
Session::flash('warning', trans('texts.plan_refunded'));
}
$hasPaid = false;
@ -241,6 +221,8 @@ class AccountController extends BaseController
$company->plan = $plan;
$company->save();
Session::flash('message', trans('texts.updated_plan'));
return Redirect::to('settings/account_management');
}
}
@ -488,23 +470,19 @@ class AccountController extends BaseController
}
}
if ($trashedCount == 0) {
return Redirect::to('gateways/create');
} else {
$tokenBillingOptions = [];
for ($i = 1; $i <= 4; $i++) {
$tokenBillingOptions[$i] = trans("texts.token_billing_{$i}");
}
return View::make('accounts.payments', [
'showAdd' => $count < count(Gateway::$alternate) + 1,
'title' => trans('texts.online_payments'),
'tokenBillingOptions' => $tokenBillingOptions,
'currency' => Utils::getFromCache(Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY),
'currencies'),
'account' => $account,
]);
$tokenBillingOptions = [];
for ($i = 1; $i <= 4; $i++) {
$tokenBillingOptions[$i] = trans("texts.token_billing_{$i}");
}
return View::make('accounts.payments', [
'showAdd' => $count < count(Gateway::$alternate) + 1,
'title' => trans('texts.online_payments'),
'tokenBillingOptions' => $tokenBillingOptions,
'currency' => Utils::getFromCache(Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY), 'currencies'),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->orderBy('rate')->get(['public_id', 'name', 'rate']),
'account' => $account,
]);
}
/**
@ -812,9 +790,12 @@ class AccountController extends BaseController
{
$account = $request->user()->account;
$account->fill($request->all());
$account->bcc_email = $request->bcc_email;
$account->save();
$settings = $account->account_email_settings;
$settings->fill($request->all());
$settings->save();
return redirect('settings/' . ACCOUNT_EMAIL_SETTINGS)
->with('message', trans('texts.updated_settings'));
}
@ -830,11 +811,11 @@ class AccountController extends BaseController
foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) {
$subjectField = "email_subject_{$type}";
$subject = Input::get($subjectField, $account->getEmailSubject($type));
$account->$subjectField = ($subject == $account->getDefaultEmailSubject($type) ? null : $subject);
$account->account_email_settings->$subjectField = ($subject == $account->getDefaultEmailSubject($type) ? null : $subject);
$bodyField = "email_template_{$type}";
$body = Input::get($bodyField, $account->getEmailTemplate($type));
$account->$bodyField = ($body == $account->getDefaultEmailTemplate($type) ? null : $body);
$account->account_email_settings->$bodyField = ($body == $account->getDefaultEmailTemplate($type) ? null : $body);
}
foreach ([REMINDER1, REMINDER2, REMINDER3] as $type) {
@ -846,6 +827,7 @@ class AccountController extends BaseController
}
$account->save();
$account->account_email_settings->save();
Session::flash('message', trans('texts.updated_settings'));
}
@ -932,6 +914,8 @@ class AccountController extends BaseController
$account->client_number_prefix = trim(Input::get('client_number_prefix'));
$account->client_number_pattern = trim(Input::get('client_number_pattern'));
$account->client_number_counter = Input::get('client_number_counter');
$account->reset_counter_frequency_id = Input::get('reset_counter_frequency_id');
$account->reset_counter_date = $account->reset_counter_frequency_id ? Utils::toSqlDate(Input::get('reset_counter_date')) : null;
if (Input::has('recurring_hour')) {
$account->recurring_hour = Input::get('recurring_hour');
@ -1054,28 +1038,32 @@ class AccountController extends BaseController
$size = filesize($filePath);
if ($size / 1000 > MAX_DOCUMENT_SIZE) {
Session::flash('warning', 'File too large');
Session::flash('warning', trans('texts.logo_warning_too_large'));
} else {
if ($documentType != 'gif') {
$account->logo = $account->account_key.'.'.$documentType;
$imageSize = getimagesize($filePath);
$account->logo_width = $imageSize[0];
$account->logo_height = $imageSize[1];
$account->logo_size = $size;
try {
$imageSize = getimagesize($filePath);
$account->logo_width = $imageSize[0];
$account->logo_height = $imageSize[1];
$account->logo_size = $size;
// make sure image isn't interlaced
if (extension_loaded('fileinfo')) {
$image = Image::make($path);
$image->interlace(false);
$imageStr = (string) $image->encode($documentType);
$disk->put($account->logo, $imageStr);
// make sure image isn't interlaced
if (extension_loaded('fileinfo')) {
$image = Image::make($path);
$image->interlace(false);
$imageStr = (string) $image->encode($documentType);
$disk->put($account->logo, $imageStr);
$account->logo_size = strlen($imageStr);
} else {
$stream = fopen($filePath, 'r');
$disk->getDriver()->putStream($account->logo, $stream, ['mimetype' => $documentTypeData['mime']]);
fclose($stream);
$account->logo_size = strlen($imageStr);
} else {
$stream = fopen($filePath, 'r');
$disk->getDriver()->putStream($account->logo, $stream, ['mimetype' => $documentTypeData['mime']]);
fclose($stream);
}
} catch (Exception $exception) {
Session::flash('warning', trans('texts.logo_warning_invalid'));
}
} else {
if (extension_loaded('fileinfo')) {
@ -1093,7 +1081,7 @@ class AccountController extends BaseController
$account->logo_width = $image->width();
$account->logo_height = $image->height();
} else {
Session::flash('warning', 'Warning: To support gifs the fileinfo PHP extension needs to be enabled.');
Session::flash('warning', trans('texts.logo_warning_fileinfo'));
}
}
}
@ -1142,9 +1130,6 @@ class AccountController extends BaseController
$user->referral_code = $this->accountRepo->getReferralCode();
}
}
if (Utils::isNinjaDev()) {
$user->dark_mode = Input::get('dark_mode') ? true : false;
}
$user->save();
@ -1189,6 +1174,8 @@ class AccountController extends BaseController
$account = Auth::user()->account;
$account->token_billing_type_id = Input::get('token_billing_type_id');
$account->auto_bill_on_due_date = boolval(Input::get('auto_bill_on_due_date'));
$account->gateway_fee_enabled = boolval(Input::get('gateway_fee_enabled'));
$account->save();
event(new UserSettingsChanged());
@ -1198,35 +1185,6 @@ class AccountController extends BaseController
return Redirect::to('settings/'.ACCOUNT_PAYMENTS);
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function savePaymentGatewayLimits()
{
$gateway_type_id = intval(Input::get('gateway_type_id'));
$gateway_settings = AccountGatewaySettings::scope()->where('gateway_type_id', '=', $gateway_type_id)->first();
if (! $gateway_settings) {
$gateway_settings = AccountGatewaySettings::createNew();
$gateway_settings->gateway_type_id = $gateway_type_id;
}
$gateway_settings->min_limit = Input::get('limit_min_enable') ? intval(Input::get('limit_min')) : null;
$gateway_settings->max_limit = Input::get('limit_max_enable') ? intval(Input::get('limit_max')) : null;
if ($gateway_settings->max_limit !== null && $gateway_settings->min_limit > $gateway_settings->max_limit) {
$gateway_settings->max_limit = $gateway_settings->min_limit;
}
$gateway_settings->save();
event(new UserSettingsChanged());
Session::flash('message', trans('texts.updated_settings'));
return Redirect::to('settings/' . ACCOUNT_PAYMENTS);
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
@ -1255,7 +1213,7 @@ class AccountController extends BaseController
public function checkEmail()
{
$email = User::withTrashed()->where('email', '=', Input::get('email'))
->where('id', '<>', Auth::user()->id)
->where('id', '<>', Auth::user()->registered ? 0 : Auth::user()->id)
->first();
if ($email) {
@ -1270,36 +1228,58 @@ class AccountController extends BaseController
*/
public function submitSignup()
{
$user = Auth::user();
$account = $user->account;
$rules = [
'new_first_name' => 'required',
'new_last_name' => 'required',
'new_password' => 'required|min:6',
'new_email' => 'email|required|unique:users,email,'.Auth::user()->id.',id',
'new_email' => 'email|required|unique:users,email',
];
if (! $user->registered) {
$rules['new_email'] .= ',' . Auth::user()->id . ',id';
}
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
return '';
}
/** @var \App\Models\User $user */
$user = Auth::user();
$user->first_name = trim(Input::get('new_first_name'));
$user->last_name = trim(Input::get('new_last_name'));
$user->email = trim(strtolower(Input::get('new_email')));
$user->username = $user->email;
$user->password = bcrypt(trim(Input::get('new_password')));
$user->registered = true;
$user->save();
$firstName = trim(Input::get('new_first_name'));
$lastName = trim(Input::get('new_last_name'));
$email = trim(strtolower(Input::get('new_email')));
$password = trim(Input::get('new_password'));
$user->account->startTrial(PLAN_PRO);
if ($user->registered) {
$newAccount = $this->accountRepo->create($firstName, $lastName, $email, $password, $account->company);
$newUser = $newAccount->users()->first();
$users = $this->accountRepo->associateAccounts($user->id, $newUser->id);
if (Input::get('go_pro') == 'true') {
Session::set(REQUESTED_PRO_PLAN, true);
Session::flash('message', trans('texts.created_new_company'));
Session::put(SESSION_USER_ACCOUNTS, $users);
Auth::loginUsingId($newUser->id);
return RESULT_SUCCESS;
} else {
$user->first_name = $firstName;
$user->last_name = $lastName;
$user->email = $email;
$user->username = $user->email;
$user->password = bcrypt($password);
$user->registered = true;
$user->save();
$user->account->startTrial(PLAN_PRO);
if (Input::get('go_pro') == 'true') {
Session::set(REQUESTED_PRO_PLAN, true);
}
return "{$user->first_name} {$user->last_name}";
}
return "{$user->first_name} {$user->last_name}";
}
/**
@ -1328,6 +1308,16 @@ class AccountController extends BaseController
return RESULT_SUCCESS;
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function purgeData()
{
$this->dispatch(new \App\Jobs\PurgeAccountData());
return redirect('/settings/account_management')->withMessage(trans('texts.purge_successful'));
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
@ -1350,6 +1340,9 @@ class AccountController extends BaseController
$account = Auth::user()->account;
\Log::info("Canceled Account: {$account->name} - {$user->email}");
$company = $account->company;
$refunded = $company->processRefund(Auth::user());
Document::scope()->each(function ($item, $key) {
$item->delete();
});
@ -1365,6 +1358,10 @@ class AccountController extends BaseController
Auth::logout();
Session::flush();
if ($refunded) {
Session::flash('warning', trans('texts.plan_refunded'));
}
return Redirect::to('/')->with('clearGuestKey', true);
}
@ -1414,18 +1411,17 @@ class AccountController extends BaseController
public function previewEmail(TemplateService $templateService)
{
$template = Input::get('template');
$invoice = Invoice::scope()
->invoices()
->withTrashed()
->first();
$invitation = \App\Models\Invitation::scope()
->with('invoice.client.contacts')
->first();
if (! $invoice) {
if (! $invitation) {
return trans('texts.create_invoice_for_sample');
}
/** @var \App\Models\Account $account */
$account = Auth::user()->account;
$invitation = $invoice->invitations->first();
$invoice = $invitation->invoice;
// replace the variables with sample data
$data = [

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Account;
use App\Models\AccountGatewaySettings;
use App\Models\AccountGateway;
use App\Models\Gateway;
use App\Services\AccountGatewayService;
@ -131,6 +132,10 @@ class AccountGatewayController extends BaseController
$currentGateways = $account->account_gateways;
$gateways = Gateway::where('payment_library_id', '=', 1)->orderBy('name')->get();
if ($accountGateway) {
$accountGateway->fields = [];
}
foreach ($gateways as $gateway) {
$fields = $gateway->getFields();
if (! $gateway->isCustom()) {
@ -372,7 +377,7 @@ class AccountGatewayController extends BaseController
'tos_agree' => 'required',
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required',
'email' => 'required|email',
];
if (WEPAY_ENABLE_CANADA) {
@ -387,6 +392,13 @@ class AccountGatewayController extends BaseController
->withInput();
}
if (! $user->email) {
$user->email = trim(Input::get('email'));
$user->first_name = trim(Input::get('first_name'));
$user->last_name = trim(Input::get('last_name'));
$user->save();
}
try {
$wepay = Utils::setupWePay();
@ -494,4 +506,33 @@ class AccountGatewayController extends BaseController
return Redirect::to("gateways/{$accountGateway->public_id}/edit");
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function savePaymentGatewayLimits()
{
$gateway_type_id = intval(Input::get('gateway_type_id'));
$gateway_settings = AccountGatewaySettings::scope()->where('gateway_type_id', '=', $gateway_type_id)->first();
if (! $gateway_settings) {
$gateway_settings = AccountGatewaySettings::createNew();
$gateway_settings->gateway_type_id = $gateway_type_id;
}
$gateway_settings->min_limit = Input::get('limit_min_enable') ? intval(Input::get('limit_min')) : null;
$gateway_settings->max_limit = Input::get('limit_max_enable') ? intval(Input::get('limit_max')) : null;
if ($gateway_settings->max_limit !== null && $gateway_settings->min_limit > $gateway_settings->max_limit) {
$gateway_settings->max_limit = $gateway_settings->min_limit;
}
$gateway_settings->fill(Input::all());
$gateway_settings->save();
Session::flash('message', trans('texts.updated_settings'));
return Redirect::to('settings/' . ACCOUNT_PAYMENTS);
}
}

View File

@ -56,7 +56,7 @@ class AppController extends BaseController
$test = Input::get('test');
$app = Input::get('app');
$app['key'] = env('APP_KEY') ?: str_random(RANDOM_KEY_LENGTH);
$app['key'] = env('APP_KEY') ?: strtolower(str_random(RANDOM_KEY_LENGTH));
$app['debug'] = Input::get('debug') ? 'true' : 'false';
$app['https'] = Input::get('https') ? 'true' : 'false';
@ -101,7 +101,7 @@ class AppController extends BaseController
$_ENV['MAIL_FROM_ADDRESS'] = $mail['from']['address'];
$_ENV['MAIL_PASSWORD'] = $mail['password'];
$_ENV['PHANTOMJS_CLOUD_KEY'] = 'a-demo-key-with-low-quota-per-ip-address';
$_ENV['PHANTOMJS_SECRET'] = str_random(RANDOM_KEY_LENGTH);
$_ENV['PHANTOMJS_SECRET'] = strtolower(str_random(RANDOM_KEY_LENGTH));
$_ENV['MAILGUN_DOMAIN'] = $mail['mailgun_domain'];
$_ENV['MAILGUN_SECRET'] = $mail['mailgun_secret'];
@ -191,7 +191,8 @@ class AppController extends BaseController
$config .= "{$key}={$val}\n";
}
$fp = fopen(base_path().'/.env', 'w');
$filePath = base_path().'/.env';
$fp = fopen($filePath, 'w');
fwrite($fp, $config);
fclose($fp);
@ -345,6 +346,16 @@ class AppController extends BaseController
return RESULT_SUCCESS;
}
public function checkData()
{
try {
Artisan::call('ninja:check-data');
return RESULT_SUCCESS;
} catch (Exception $exception) {
return RESULT_FAILURE;
}
}
public function stats()
{
if (! hash_equals(Input::get('password'), env('RESELLER_PASSWORD'))) {

View File

@ -173,13 +173,17 @@ class AuthController extends Controller
public function getLogoutWrapper()
{
if (Auth::check() && ! Auth::user()->registered) {
$account = Auth::user()->account;
$this->accountRepo->unlinkAccount($account);
if (request()->force_logout) {
$account = Auth::user()->account;
$this->accountRepo->unlinkAccount($account);
if (! $account->hasMultipleAccounts()) {
$account->company->forceDelete();
if (! $account->hasMultipleAccounts()) {
$account->company->forceDelete();
}
$account->forceDelete();
} else {
return redirect('/');
}
$account->forceDelete();
}
$response = self::getLogout();

View File

@ -20,6 +20,7 @@ use Utils;
* schemes={"http","https"},
* host="ninja.dev",
* basePath="/api/v1",
* produces={"application/json"},
* @SWG\Info(
* version="1.0.0",
* title="Invoice Ninja API",
@ -37,11 +38,12 @@ use Utils;
* description="Find out more about Invoice Ninja",
* url="https://www.invoiceninja.com"
* ),
* security={"api_key": {}},
* @SWG\SecurityScheme(
* securityDefinition="api_key",
* type="apiKey",
* in="header",
* name="TOKEN"
* name="X-Ninja-Token"
* )
* )
*/

View File

@ -37,7 +37,7 @@ class BaseController extends Controller
// when restoring redirect to entity
if ($action == 'restore' && count($ids) == 1) {
return redirect("{$entityTypes}/" . $ids[0]);
return redirect("{$entityTypes}/" . $ids[0] . '/edit');
// when viewing from a datatable list
} elseif (strpos($referer, '/clients/')) {
return redirect($referer);
@ -45,7 +45,7 @@ class BaseController extends Controller
return redirect("{$entityTypes}");
// when viewing individual entity
} elseif (count($ids)) {
return redirect("{$entityTypes}/" . $ids[0]);
return redirect("{$entityTypes}/" . $ids[0] . '/edit');
} else {
return redirect("{$entityTypes}");
}

View File

@ -26,11 +26,12 @@ class ClientApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/clients",
* summary="List of clients",
* summary="List clients",
* operationId="listClients",
* tags={"client"},
* @SWG\Response(
* response=200,
* description="A list with clients",
* description="A list of clients",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Client"))
* ),
* @SWG\Response(
@ -45,11 +46,12 @@ class ClientApiController extends BaseAPIController
->orderBy('created_at', 'desc')
->withTrashed();
// Filter by email
if ($email = Input::get('email')) {
$clients = $clients->whereHas('contacts', function ($query) use ($email) {
$query->where('email', $email);
});
} elseif ($idNumber = Input::get('id_number')) {
$clients = $clients->whereIdNumber($idNumber);
}
return $this->listResponse($clients);
@ -58,8 +60,15 @@ class ClientApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/clients/{client_id}",
* summary="Individual Client",
* summary="Retrieve a client",
* operationId="getClient",
* tags={"client"},
* @SWG\Parameter(
* in="path",
* name="client_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single client",
@ -79,11 +88,12 @@ class ClientApiController extends BaseAPIController
/**
* @SWG\Post(
* path="/clients",
* tags={"client"},
* summary="Create a client",
* operationId="createClient",
* tags={"client"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="client",
* @SWG\Schema(ref="#/definitions/Client")
* ),
* @SWG\Response(
@ -107,16 +117,23 @@ class ClientApiController extends BaseAPIController
/**
* @SWG\Put(
* path="/clients/{client_id}",
* tags={"client"},
* summary="Update a client",
* operationId="updateClient",
* tags={"client"},
* @SWG\Parameter(
* in="path",
* name="client_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="client",
* @SWG\Schema(ref="#/definitions/Client")
* ),
* @SWG\Response(
* response=200,
* description="Update client",
* description="Updated client",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Client"))
* ),
* @SWG\Response(
@ -145,16 +162,18 @@ class ClientApiController extends BaseAPIController
/**
* @SWG\Delete(
* path="/clients/{client_id}",
* tags={"client"},
* summary="Delete a client",
* operationId="deleteClient",
* tags={"client"},
* @SWG\Parameter(
* in="body",
* name="body",
* @SWG\Schema(ref="#/definitions/Client")
* in="path",
* name="client_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Delete client",
* description="Deleted client",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Client"))
* ),
* @SWG\Response(

View File

@ -68,6 +68,7 @@ class ClientPortalController extends BaseController
}
$account->loadLocalizationSettings($client);
$this->invoiceRepo->clearGatewayFee($invoice);
if (! Input::has('phantomjs') && ! session('silent:' . $client->id) && ! Session::has($invitation->invitation_key)
&& (! Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
@ -146,6 +147,7 @@ class ClientPortalController extends BaseController
'paymentTypes' => $paymentTypes,
'paymentURL' => $paymentURL,
'phantomjs' => Input::has('phantomjs'),
'gatewayTypeId' => count($paymentTypes) == 1 ? $paymentTypes[0]['gatewayTypeId'] : false,
];
if ($paymentDriver = $account->paymentDriver($invitation, GATEWAY_TYPE_CREDIT_CARD)) {
@ -521,7 +523,7 @@ class ClientPortalController extends BaseController
'account' => $account,
'title' => trans('texts.credits'),
'entityType' => ENTITY_CREDIT,
'columns' => Utils::trans(['credit_date', 'credit_amount', 'credit_balance']),
'columns' => Utils::trans(['credit_date', 'credit_amount', 'credit_balance', 'notes']),
];
return response()->view('public_list', $data);

View File

@ -0,0 +1,182 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Http\Requests\CreateContactRequest;
use App\Http\Requests\UpdateContactRequest;
use App\Models\Contact;
use App\Ninja\Repositories\ContactRepository;
use Input;
use Response;
use Utils;
use App\Services\ContactService;
class ContactApiController extends BaseAPIController
{
protected $contactRepo;
protected $contactService;
protected $entityType = ENTITY_CONTACT;
public function __construct(ContactRepository $contactRepo, ContactService $contactService)
{
parent::__construct();
$this->contactRepo = $contactRepo;
$this->contactService = $contactService;
}
/**
* @SWG\Get(
* path="/contacts",
* summary="List contacts",
* tags={"contact"},
* @SWG\Response(
* response=200,
* description="A list of contacts",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Contact"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function index()
{
$contacts = Contact::scope()
->withTrashed()
->orderBy('created_at', 'desc');
return $this->listResponse($contacts);
}
/**
* @SWG\Get(
* path="/contacts/{contact_id}",
* summary="Retrieve a contact",
* tags={"contact"},
* @SWG\Parameter(
* in="path",
* name="contact_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single contact",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Contact"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(ContactRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/contacts",
* tags={"contact"},
* summary="Create a contact",
* @SWG\Parameter(
* in="body",
* name="contact",
* @SWG\Schema(ref="#/definitions/Contact")
* ),
* @SWG\Response(
* response=200,
* description="New contact",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Contact"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function store(CreateContactRequest $request)
{
$contact = $this->contactService->save($request->input());
return $this->itemResponse($contact);
}
/**
* @SWG\Put(
* path="/contacts/{contact_id}",
* tags={"contact"},
* summary="Update a contact",
* @SWG\Parameter(
* in="path",
* name="contact_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="contact",
* @SWG\Schema(ref="#/definitions/Contact")
* ),
* @SWG\Response(
* response=200,
* description="Updated contact",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Contact"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*
* @param mixed $publicId
*/
public function update(UpdateContactRequest $request, $publicId)
{
if ($request->action) {
return $this->handleAction($request);
}
$data = $request->input();
$data['public_id'] = $publicId;
$contact = $this->contactService->save($data, $request->entity());
return $this->itemResponse($contact);
}
/**
* @SWG\Delete(
* path="/contacts/{contact_id}",
* tags={"contact"},
* summary="Delete a contact",
* @SWG\Parameter(
* in="path",
* name="contact_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted contact",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Contact"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(UpdateContactRequest $request)
{
$contact = $request->entity();
$this->contactRepo->delete($contact);
return $this->itemResponse($contact);
}
}

View File

@ -2,8 +2,9 @@
namespace App\Http\Controllers;
use App\Http\Requests\CreateDocumentRequest;
use App\Http\Requests\DocumentRequest;
use App\Http\Requests\CreateDocumentRequest;
use App\Http\Requests\UpdateDocumentRequest;
use App\Models\Document;
use App\Ninja\Repositories\DocumentRepository;
@ -37,11 +38,12 @@ class DocumentAPIController extends BaseAPIController
/**
* @SWG\Get(
* path="/documents",
* summary="List of document",
* summary="List document",
* operationId="listDocuments",
* tags={"document"},
* @SWG\Response(
* response=200,
* description="A list with documents",
* description="A list of documents",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Document"))
* ),
* @SWG\Response(
@ -61,6 +63,29 @@ class DocumentAPIController extends BaseAPIController
* @param DocumentRequest $request
*
* @return \Illuminate\Http\Response|\Redirect|\Symfony\Component\HttpFoundation\StreamedResponse
*
* @SWG\Get(
* path="/documents/{document_id}",
* summary="Download a document",
* operationId="getDocument",
* tags={"document"},
* produces={"application/octet-stream"},
* @SWG\Parameter(
* in="path",
* name="document_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A file",
* @SWG\Schema(type="file")
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(DocumentRequest $request)
{
@ -76,11 +101,12 @@ class DocumentAPIController extends BaseAPIController
/**
* @SWG\Post(
* path="/documents",
* tags={"document"},
* summary="Create a document",
* operationId="createDocument",
* tags={"document"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="document",
* @SWG\Schema(ref="#/definitions/Document")
* ),
* @SWG\Response(
@ -100,4 +126,36 @@ class DocumentAPIController extends BaseAPIController
return $this->itemResponse($document);
}
/**
* @SWG\Delete(
* path="/documents/{document_id}",
* summary="Delete a document",
* operationId="deleteDocument",
* tags={"document"},
* @SWG\Parameter(
* in="path",
* name="document_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted document",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Document"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(UpdateDocumentRequest $request)
{
$entity = $request->entity();
$this->documentRepo->delete($entity);
return $this->itemResponse($entity);
}
}

View File

@ -2,8 +2,8 @@
namespace App\Http\Controllers;
use App\Http\Requests\CreateExpenseRequest;
use App\Http\Requests\ExpenseRequest;
use App\Http\Requests\CreateExpenseRequest;
use App\Http\Requests\UpdateExpenseRequest;
use App\Models\Expense;
use App\Ninja\Repositories\ExpenseRepository;
@ -28,11 +28,12 @@ class ExpenseApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/expenses",
* summary="List of expenses",
* summary="List expenses",
* operationId="listExpenses",
* tags={"expense"},
* @SWG\Response(
* response=200,
* description="A list with expenses",
* description="A list of expenses",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Expense"))
* ),
* @SWG\Response(
@ -51,14 +52,43 @@ class ExpenseApiController extends BaseAPIController
return $this->listResponse($expenses);
}
/**
* @SWG\Get(
* path="/expenses/{expense_id}",
* summary="Retrieve an expense",
* operationId="getExpense",
* tags={"expense"},
* @SWG\Parameter(
* in="path",
* name="expense_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single expense",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(ExpenseRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/expenses",
* summary="Create an expense",
* operationId="createExpense",
* tags={"expense"},
* summary="Create a expense",
* @SWG\Parameter(
* in="body",
* name="body",
* name="expense",
* @SWG\Schema(ref="#/definitions/Expense")
* ),
* @SWG\Response(
@ -86,16 +116,23 @@ class ExpenseApiController extends BaseAPIController
/**
* @SWG\Put(
* path="/expenses/{expense_id}",
* summary="Update an expense",
* operationId="updateExpense",
* tags={"expense"},
* summary="Update a expense",
* @SWG\Parameter(
* in="path",
* name="expense_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="expense",
* @SWG\Schema(ref="#/definitions/Expense")
* ),
* @SWG\Response(
* response=200,
* description="Update expense",
* description="Updated expense",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense"))
* ),
* @SWG\Response(
@ -122,16 +159,18 @@ class ExpenseApiController extends BaseAPIController
/**
* @SWG\Delete(
* path="/expenses/{expense_id}",
* summary="Delete an expense",
* operationId="deleteExpense",
* tags={"expense"},
* summary="Delete a expense",
* @SWG\Parameter(
* in="body",
* name="body",
* @SWG\Schema(ref="#/definitions/Expense")
* in="path",
* name="expense_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Delete expense",
* description="Deleted expense",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense"))
* ),
* @SWG\Response(
@ -140,7 +179,7 @@ class ExpenseApiController extends BaseAPIController
* )
* )
*/
public function destroy(ExpenseRequest $request)
public function destroy(UpdateExpenseRequest $request)
{
$expense = $request->entity();

View File

@ -2,8 +2,10 @@
namespace App\Http\Controllers;
use App\Http\Requests\ExpenseCategoryRequest;
use App\Http\Requests\CreateExpenseCategoryRequest;
use App\Http\Requests\UpdateExpenseCategoryRequest;
use App\Models\ExpenseCategory;
use App\Ninja\Repositories\ExpenseCategoryRepository;
use App\Services\ExpenseCategoryService;
use Input;
@ -22,14 +24,69 @@ class ExpenseCategoryApiController extends BaseAPIController
$this->categoryService = $categoryService;
}
/**
* @SWG\Get(
* path="/expense_categories",
* summary="List expense categories",
* operationId="listExpenseCategories",
* tags={"expense_category"},
* @SWG\Response(
* response=200,
* description="A list of expense categories",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/ExpenseCategory"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function index()
{
$clients = ExpenseCategory::scope()
->orderBy('created_at', 'desc')
->withTrashed();
return $this->listResponse($clients);
}
/**
* @SWG\Get(
* path="/expense_categories/{expense_category_id}",
* summary="Retrieve an Expense Category",
* operationId="getExpenseCategory",
* tags={"expense_category"},
* @SWG\Parameter(
* in="path",
* name="expense_category_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single expense categroy",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/ExpenseCategory"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(ExpenseCategory $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/expense_categories",
* tags={"expense_category"},
* summary="Create an expense category",
* operationId="createExpenseCategory",
* tags={"expense_category"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="expense_category",
* @SWG\Schema(ref="#/definitions/ExpenseCategory")
* ),
* @SWG\Response(
@ -53,16 +110,23 @@ class ExpenseCategoryApiController extends BaseAPIController
/**
* @SWG\Put(
* path="/expense_categories/{expense_category_id}",
* tags={"expense_category"},
* summary="Update an expense category",
* operationId="updateExpenseCategory",
* tags={"expense_category"},
* @SWG\Parameter(
* in="path",
* name="expense_category_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="expense_category",
* @SWG\Schema(ref="#/definitions/ExpenseCategory")
* ),
* @SWG\Response(
* response=200,
* description="Update expense category",
* description="Updated expense category",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/ExpenseCategory"))
* ),
* @SWG\Response(
@ -77,4 +141,36 @@ class ExpenseCategoryApiController extends BaseAPIController
return $this->itemResponse($category);
}
/**
* @SWG\Delete(
* path="/expense_categories/{expense_category_id}",
* summary="Delete an expense category",
* operationId="deleteExpenseCategory",
* tags={"expense_category"},
* @SWG\Parameter(
* in="path",
* name="expense_category_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted expense category",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/ExpenseCategory"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(UpdateExpenseCategoryRequest $request)
{
$entity = $request->entity();
$this->expenseCategoryRepo->delete($entity);
return $this->itemResponse($entity);
}
}

View File

@ -65,12 +65,6 @@ class HomeController extends BaseController
*/
public function invoiceNow()
{
if (Auth::check() && Input::get('new_company')) {
Session::put(PREV_USER_ID, Auth::user()->id);
Auth::user()->clearSession();
Auth::logout();
}
// Track the referral/campaign code
if (Input::has('rc')) {
Session::set(SESSION_REFERRAL_CODE, Input::get('rc'));

View File

@ -2,57 +2,92 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\ImportService;
use App\Jobs\ImportData;
use Exception;
use Input;
use Redirect;
use Session;
use Utils;
use View;
use Auth;
class ImportController extends BaseController
{
public function __construct(ImportService $importService)
{
//parent::__construct();
$this->importService = $importService;
}
public function doImport()
public function doImport(Request $request)
{
if (! Auth::user()->confirmed) {
return redirect('/settings/' . ACCOUNT_IMPORT_EXPORT)->withError(trans('texts.confirm_account_to_import'));
}
$source = Input::get('source');
$files = [];
$timestamp = time();
foreach (ImportService::$entityTypes as $entityType) {
if (Input::file("{$entityType}_file")) {
$files[$entityType] = Input::file("{$entityType}_file")->getRealPath();
if ($source === IMPORT_CSV) {
Session::forget("{$entityType}-data");
$fileName = $entityType;
if ($request->hasFile($fileName)) {
$file = $request->file($fileName);
$destinationPath = storage_path() . '/import';
$extension = $file->getClientOriginalExtension();
if (! in_array($extension, ['csv', 'xls', 'xlsx', 'json'])) {
continue;
}
$newFileName = sprintf('%s_%s_%s.%s', Auth::user()->account_id, $timestamp, $fileName, $extension);
$file->move($destinationPath, $newFileName);
$files[$entityType] = $newFileName;
}
}
if (! count($files)) {
Session::flash('error', trans('texts.select_file'));
return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT);
}
try {
if ($source === IMPORT_CSV) {
$data = $this->importService->mapCSV($files);
return View::make('accounts.import_map', ['data' => $data]);
return View::make('accounts.import_map', [
'data' => $data,
'timestamp' => $timestamp,
]);
} elseif ($source === IMPORT_JSON) {
$results = $this->importService->importJSON($files[IMPORT_JSON]);
return $this->showResult($results);
$includeData = filter_var(Input::get('data'), FILTER_VALIDATE_BOOLEAN);
$includeSettings = filter_var(Input::get('settings'), FILTER_VALIDATE_BOOLEAN);
if (config('queue.default') === 'sync') {
$results = $this->importService->importJSON($files[IMPORT_JSON], $includeData, $includeSettings);
$message = $this->importService->presentResults($results, $includeSettings);
} else {
$settings = [
'files' => $files,
'include_data' => $includeData,
'include_settings' => $includeSettings,
];
$this->dispatch(new ImportData(Auth::user(), IMPORT_JSON, $settings));
$message = trans('texts.import_started');
}
} else {
$results = $this->importService->importFiles($source, $files);
return $this->showResult($results);
if (config('queue.default') === 'sync') {
$results = $this->importService->importFiles($source, $files);
$message = $this->importService->presentResults($results);
} else {
$settings = [
'files' => $files,
'source' => $source,
];
$this->dispatch(new ImportData(Auth::user(), false, $settings));
$message = trans('texts.import_started');
}
}
return redirect('/settings/' . ACCOUNT_IMPORT_EXPORT)->withWarning($message);
} catch (Exception $exception) {
Utils::logError($exception);
Session::flash('error', $exception->getMessage());
@ -63,13 +98,24 @@ class ImportController extends BaseController
public function doImportCSV()
{
$map = Input::get('map');
$headers = Input::get('headers');
try {
$results = $this->importService->importCSV($map, $headers);
$map = Input::get('map');
$headers = Input::get('headers');
$timestamp = Input::get('timestamp');
if (config('queue.default') === 'sync') {
$results = $this->importService->importCSV($map, $headers, $timestamp);
$message = $this->importService->presentResults($results);
} else {
$settings = [
'timestamp' => $timestamp,
'map' => $map,
'headers' => $headers,
];
$this->dispatch(new ImportData(Auth::user(), IMPORT_CSV, $settings));
$message = trans('texts.import_started');
}
return $this->showResult($results);
return redirect('/settings/' . ACCOUNT_IMPORT_EXPORT)->withWarning($message);
} catch (Exception $exception) {
Utils::logError($exception);
Session::flash('error', $exception->getMessage());
@ -77,32 +123,4 @@ class ImportController extends BaseController
return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT);
}
}
private function showResult($results)
{
$message = '';
$skipped = [];
foreach ($results as $entityType => $entityResults) {
if ($count = count($entityResults[RESULT_SUCCESS])) {
$message .= trans("texts.created_{$entityType}s", ['count' => $count]) . '<br/>';
}
if (count($entityResults[RESULT_FAILURE])) {
$skipped = array_merge($skipped, $entityResults[RESULT_FAILURE]);
}
}
if (count($skipped)) {
$message .= '<p/>' . trans('texts.failed_to_import') . '<br/>';
foreach ($skipped as $skip) {
$message .= json_encode($skip) . '<br/>';
}
}
if ($message) {
Session::flash('warning', $message);
}
return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT);
}
}

View File

@ -2,8 +2,8 @@
namespace App\Http\Controllers;
use App\Http\Requests\CreateInvoiceAPIRequest;
use App\Http\Requests\InvoiceRequest;
use App\Http\Requests\CreateInvoiceAPIRequest;
use App\Http\Requests\UpdateInvoiceAPIRequest;
use App\Jobs\SendInvoiceEmail;
use App\Jobs\SendPaymentEmail;
@ -42,11 +42,12 @@ class InvoiceApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/invoices",
* summary="List of invoices",
* summary="List invoices",
* operationId="listInvoices",
* tags={"invoice"},
* @SWG\Response(
* response=200,
* description="A list with invoices",
* description="A list of invoices",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice"))
* ),
* @SWG\Response(
@ -62,14 +63,25 @@ class InvoiceApiController extends BaseAPIController
->with('invoice_items', 'client')
->orderBy('created_at', 'desc');
// Filter by invoice number
if ($invoiceNumber = Input::get('invoice_number')) {
$invoices->whereInvoiceNumber($invoiceNumber);
}
return $this->listResponse($invoices);
}
/**
* @SWG\Get(
* path="/invoices/{invoice_id}",
* summary="Individual Invoice",
* summary="Retrieve an Invoice",
* tags={"invoice"},
* @SWG\Parameter(
* in="path",
* name="invoice_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single invoice",
@ -89,11 +101,11 @@ class InvoiceApiController extends BaseAPIController
/**
* @SWG\Post(
* path="/invoices",
* tags={"invoice"},
* summary="Create an invoice",
* tags={"invoice"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="invoice",
* @SWG\Schema(ref="#/definitions/Invoice")
* ),
* @SWG\Response(
@ -297,27 +309,38 @@ class InvoiceApiController extends BaseAPIController
{
$invoice = $request->entity();
$this->dispatch(new SendInvoiceEmail($invoice));
//$this->dispatch(new SendInvoiceEmail($invoice));
$result = app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice);
if ($result !== true) {
return $this->errorResponse($result, 500);
}
$response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT);
$headers = Utils::getApiHeaders();
$response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT);
return Response::make($response, 200, $headers);
}
/**
* @SWG\Put(
* path="/invoices",
* tags={"invoice"},
* path="/invoices/{invoice_id}",
* summary="Update an invoice",
* tags={"invoice"},
* @SWG\Parameter(
* in="path",
* name="invoice_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="invoice",
* @SWG\Schema(ref="#/definitions/Invoice")
* ),
* @SWG\Response(
* response=200,
* description="Update invoice",
* description="Updated invoice",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice"))
* ),
* @SWG\Response(
@ -352,17 +375,18 @@ class InvoiceApiController extends BaseAPIController
/**
* @SWG\Delete(
* path="/invoices",
* tags={"invoice"},
* path="/invoices/{invoice_id}",
* summary="Delete an invoice",
* tags={"invoice"},
* @SWG\Parameter(
* in="body",
* name="body",
* @SWG\Schema(ref="#/definitions/Invoice")
* in="path",
* name="invoice_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Delete invoice",
* description="Deleted invoice",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice"))
* ),
* @SWG\Response(

View File

@ -289,7 +289,7 @@ class InvoiceController extends BaseController
$taxRateOptions = $account->present()->taxRateOptions;
if ($invoice->exists) {
foreach ($invoice->getTaxes() as $key => $rate) {
$key = '0 ' . $key;
$key = '0 ' . $key; // mark it as a standard exclusive rate option
if (isset($taxRateOptions[$key])) {
continue;
}

View File

@ -302,6 +302,7 @@ class OnlinePaymentController extends BaseController
}
Auth::onceUsingId($account->users[0]->id);
$account->loadLocalizationSettings();
$product = Product::scope(Input::get('product_id'))->first();
if (! $product) {
@ -330,6 +331,8 @@ class OnlinePaymentController extends BaseController
$data = [
'currency_id' => $account->currency_id,
'contact' => Input::all(),
'custom_value1' => Input::get('custom_client1'),
'custom_value2' => Input::get('custom_client2'),
];
$client = $clientRepo->save($data, $client);
}
@ -343,6 +346,8 @@ class OnlinePaymentController extends BaseController
'start_date' => Input::get('start_date', date('Y-m-d')),
'tax_rate1' => $account->default_tax_rate ? $account->default_tax_rate->rate : 0,
'tax_name1' => $account->default_tax_rate ? $account->default_tax_rate->name : '',
'custom_text_value1' => Input::get('custom_invoice1'),
'custom_text_value2' => Input::get('custom_invoice2'),
'invoice_items' => [[
'product_key' => $product->product_key,
'notes' => $product->notes,
@ -350,6 +355,8 @@ class OnlinePaymentController extends BaseController
'qty' => 1,
'tax_rate1' => $product->default_tax_rate ? $product->default_tax_rate->rate : 0,
'tax_name1' => $product->default_tax_rate ? $product->default_tax_rate->name : '',
'custom_value1' => Input::get('custom_product1') ?: $product->custom_value1,
'custom_value2' => Input::get('custom_product2') ?: $product->custom_value2,
]],
];
$invoice = $invoiceService->save($data);
@ -364,9 +371,15 @@ class OnlinePaymentController extends BaseController
}
if ($gatewayTypeAlias) {
return redirect()->to($invitation->getLink('payment') . "/{$gatewayTypeAlias}");
$link = $invitation->getLink('payment') . "/{$gatewayTypeAlias}";
} else {
return redirect()->to($invitation->getLink());
$link = $invitation->getLink();
}
if (filter_var(Input::get('return_link'), FILTER_VALIDATE_BOOLEAN)) {
return $link;
} else {
return redirect()->to($link);
}
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\PaymentRequest;
use App\Http\Requests\CreatePaymentAPIRequest;
use App\Http\Requests\UpdatePaymentRequest;
use App\Models\Invoice;
@ -28,11 +29,12 @@ class PaymentApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/payments",
* summary="List payments",
* operationId="listPayments",
* tags={"payment"},
* summary="List of payments",
* @SWG\Response(
* response=200,
* description="A list with payments",
* description="A list of payments",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Payment"))
* ),
* @SWG\Response(
@ -52,18 +54,20 @@ class PaymentApiController extends BaseAPIController
}
/**
* @SWG\Put(
* path="/payments/{payment_id",
* summary="Update a payment",
* @SWG\Get(
* path="/payments/{payment_id}",
* summary="Retrieve a payment",
* operationId="getPayment",
* tags={"payment"},
* @SWG\Parameter(
* in="body",
* name="body",
* @SWG\Schema(ref="#/definitions/Payment")
* in="path",
* name="payment_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Update payment",
* description="A single payment",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment"))
* ),
* @SWG\Response(
@ -71,30 +75,21 @@ class PaymentApiController extends BaseAPIController
* description="an ""unexpected"" error"
* )
* )
*
* @param mixed $publicId
*/
public function update(UpdatePaymentRequest $request, $publicId)
public function show(PaymentRequest $request)
{
if ($request->action) {
return $this->handleAction($request);
}
$data = $request->input();
$data['public_id'] = $publicId;
$payment = $this->paymentRepo->save($data, $request->entity());
return $this->itemResponse($payment);
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/payments",
* summary="Create a payment",
* operationId="createPayment",
* tags={"payment"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="payment",
* @SWG\Schema(ref="#/definitions/Payment")
* ),
* @SWG\Response(
@ -123,18 +118,63 @@ class PaymentApiController extends BaseAPIController
}
/**
* @SWG\Delete(
* @SWG\Put(
* path="/payments/{payment_id}",
* summary="Delete a payment",
* summary="Update a payment",
* operationId="updatePayment",
* tags={"payment"},
* @SWG\Parameter(
* in="path",
* name="payment_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="payment",
* @SWG\Schema(ref="#/definitions/Payment")
* ),
* @SWG\Response(
* response=200,
* description="Delete payment",
* description="Updated payment",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*
* @param mixed $publicId
*/
public function update(UpdatePaymentRequest $request, $publicId)
{
if ($request->action) {
return $this->handleAction($request);
}
$data = $request->input();
$data['public_id'] = $publicId;
$payment = $this->paymentRepo->save($data, $request->entity());
return $this->itemResponse($payment);
}
/**
* @SWG\Delete(
* path="/payments/{payment_id}",
* summary="Delete a payment",
* operationId="deletePayment",
* tags={"payment"},
* @SWG\Parameter(
* in="path",
* name="payment_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted payment",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment"))
* ),
* @SWG\Response(
@ -147,7 +187,7 @@ class PaymentApiController extends BaseAPIController
{
$payment = $request->entity();
$this->clientRepo->delete($payment);
$this->paymentRepo->delete($payment);
return $this->itemResponse($payment);
}

View File

@ -6,6 +6,7 @@ use App\Http\Requests\CreatePaymentRequest;
use App\Http\Requests\PaymentRequest;
use App\Http\Requests\UpdatePaymentRequest;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice;
use App\Ninja\Datatables\PaymentDatatable;
use App\Ninja\Mailers\ContactMailer;
@ -89,7 +90,7 @@ class PaymentController extends BaseController
{
$invoices = Invoice::scope()
->invoices()
->where('invoices.balance', '>', 0)
->where('invoices.balance', '!=', 0)
->with('client', 'invoice_status')
->orderBy('invoice_number')->get();
@ -180,17 +181,28 @@ class PaymentController extends BaseController
{
// check payment has been marked sent
$request->invoice->markSentIfUnsent();
$input = $request->input();
$input['invoice_id'] = Invoice::getPrivateId($input['invoice']);
$input['client_id'] = Client::getPrivateId($input['client']);
$payment = $this->paymentRepo->save($input);
$amount = Utils::parseFloat($input['amount']);
$credit = false;
// if the payment amount is more than the balance create a credit
if ($amount > $request->invoice->balance) {
$credit = Credit::createNew();
$credit->client_id = $request->invoice->client_id;
$credit->credit_date = date_create()->format('Y-m-d');
$credit->amount = $credit->balance = $amount - $request->invoice->balance;
$credit->private_notes = trans('texts.credit_created_by', ['transaction_reference' => $input['transaction_reference']]);
$credit->save();
$input['amount'] = $request->invoice->balance;
}
$payment = $this->paymentService->save($input);
if (Input::get('email_receipt')) {
$this->contactMailer->sendPaymentConfirmation($payment);
Session::flash('message', trans('texts.created_payment_emailed_client'));
Session::flash('message', trans($credit ? 'texts.created_payment_and_credit_emailed_client' : 'texts.created_payment_emailed_client'));
} else {
Session::flash('message', trans('texts.created_payment'));
Session::flash('message', trans($credit ? 'texts.created_payment_and_credit' : 'texts.created_payment'));
}
return redirect()->to($payment->client->getRoute() . '#payments');

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\ProductRequest;
use App\Http\Requests\CreateProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Models\Product;
@ -37,11 +38,12 @@ class ProductApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/products",
* summary="List of products",
* summary="List products",
* operationId="listProducts",
* tags={"product"},
* @SWG\Response(
* response=200,
* description="A list with products",
* description="A list of products",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Product"))
* ),
* @SWG\Response(
@ -59,11 +61,40 @@ class ProductApiController extends BaseAPIController
return $this->listResponse($products);
}
/**
* @SWG\Get(
* path="/products/{product_id}",
* summary="Retrieve a product",
* operationId="getProduct",
* tags={"product"},
* @SWG\Parameter(
* in="path",
* name="product_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single product",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Product"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(ProductRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/products",
* tags={"product"},
* summary="Create a product",
* operationId="createProduct",
* tags={"product"},
* @SWG\Parameter(
* in="body",
* name="body",
@ -90,16 +121,23 @@ class ProductApiController extends BaseAPIController
/**
* @SWG\Put(
* path="/products/{product_id}",
* tags={"product"},
* summary="Update a product",
* operationId="updateProduct",
* tags={"product"},
* @SWG\Parameter(
* in="path",
* name="product_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="product",
* @SWG\Schema(ref="#/definitions/Product")
* ),
* @SWG\Response(
* response=200,
* description="Update product",
* description="Updated product",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Product"))
* ),
* @SWG\Response(
@ -122,4 +160,36 @@ class ProductApiController extends BaseAPIController
return $this->itemResponse($product);
}
/**
* @SWG\Delete(
* path="/products/{product_id}",
* summary="Delete a product",
* operationId="deleteProduct",
* tags={"product"},
* @SWG\Parameter(
* in="path",
* name="product_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted product",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Product"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(UpdateProductRequest $request)
{
$product = $request->entity();
$this->productRepo->delete($product);
return $this->itemResponse($product);
}
}

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\TaxRate;
use App\Ninja\Datatables\ProductDatatable;
use App\Ninja\Repositories\ProductRepository;
use App\Services\ProductService;
use Auth;
use Input;
@ -24,16 +25,22 @@ class ProductController extends BaseController
*/
protected $productService;
/**
* @var ProductRepository
*/
protected $productRepo;
/**
* ProductController constructor.
*
* @param ProductService $productService
*/
public function __construct(ProductService $productService)
public function __construct(ProductService $productService, ProductRepository $productRepo)
{
//parent::__construct();
$this->productService = $productService;
$this->productRepo = $productRepo;
}
/**
@ -137,11 +144,7 @@ class ProductController extends BaseController
$product = Product::createNew();
}
$product->product_key = trim(Input::get('product_key'));
$product->notes = trim(Input::get('notes'));
$product->cost = trim(Input::get('cost'));
$product->fill(Input::all());
$product->save();
$this->productRepo->save(Input::all(), $product);
$message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product');
Session::flash('message', $message);

View File

@ -6,7 +6,7 @@ use App\Models\Invoice;
use App\Ninja\Repositories\InvoiceRepository;
use Response;
class QuoteApiController extends BaseAPIController
class QuoteApiController extends InvoiceAPIController
{
protected $invoiceRepo;
@ -19,22 +19,23 @@ class QuoteApiController extends BaseAPIController
$this->invoiceRepo = $invoiceRepo;
}
/**
* @SWG\Get(
* path="/quotes",
* tags={"quote"},
* summary="List of quotes",
* @SWG\Response(
* response=200,
* description="A list with quotes",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
/**
* @SWG\Get(
* path="/quotes",
* summary="List quotes",
* operationId="listQuotes",
* tags={"quote"},
* @SWG\Response(
* response=200,
* description="A list of quotes",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function index()
{
$invoices = Invoice::scope()

View File

@ -67,6 +67,7 @@ class ReportController extends BaseController
}
$reportTypes = [
'activity',
'aging',
'client',
'expense',
@ -76,6 +77,7 @@ class ReportController extends BaseController
'profit_and_loss',
'task',
'tax_rate',
'quote',
];
$params = [

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\TaskRequest;
use App\Http\Requests\UpdateTaskRequest;
use App\Models\Task;
use App\Ninja\Repositories\TaskRepository;
@ -26,11 +27,12 @@ class TaskApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/tasks",
* summary="List tasks",
* operationId="listTasks",
* tags={"task"},
* summary="List of tasks",
* @SWG\Response(
* response=200,
* description="A list with tasks",
* description="A list of tasks",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Task"))
* ),
* @SWG\Response(
@ -49,14 +51,43 @@ class TaskApiController extends BaseAPIController
return $this->listResponse($tasks);
}
/**
* @SWG\Get(
* path="/tasks/{task_id}",
* summary="Retrieve a task",
* operationId="getTask",
* tags={"task"},
* @SWG\Parameter(
* in="path",
* name="task_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single task",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Task"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(TaskRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/tasks",
* tags={"task"},
* summary="Create a task",
* operationId="createTask",
* tags={"task"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="task",
* @SWG\Schema(ref="#/definitions/Task")
* ),
* @SWG\Response(
@ -90,9 +121,16 @@ class TaskApiController extends BaseAPIController
/**
* @SWG\Put(
* path="/task/{task_id}",
* tags={"task"},
* path="/tasks/{task_id}",
* summary="Update a task",
* operationId="updateTask",
* tags={"task"},
* @SWG\Parameter(
* in="path",
* name="task_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
@ -117,4 +155,36 @@ class TaskApiController extends BaseAPIController
return $this->itemResponse($task);
}
/**
* @SWG\Delete(
* path="/tasks/{task_id}",
* summary="Delete a task",
* operationId="deleteTask",
* tags={"task"},
* @SWG\Parameter(
* in="path",
* name="task_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted task",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Task"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(UpdateTaskRequest $request)
{
$task = $request->entity();
$this->taskRepo->delete($task);
return $this->itemResponse($task);
}
}

View File

@ -96,7 +96,7 @@ class TaskController extends BaseController
*/
public function store(CreateTaskRequest $request)
{
return $this->save();
return $this->save($request);
}
/**
@ -202,7 +202,7 @@ class TaskController extends BaseController
{
$task = $request->entity();
return $this->save($task->public_id);
return $this->save($request, $task->public_id);
}
/**
@ -222,7 +222,7 @@ class TaskController extends BaseController
*
* @return \Illuminate\Http\RedirectResponse
*/
private function save($publicId = null)
private function save($request, $publicId = null)
{
$action = Input::get('action');
@ -230,7 +230,7 @@ class TaskController extends BaseController
return self::bulk();
}
$task = $this->taskRepo->save($publicId, Input::all());
$task = $this->taskRepo->save($publicId, $request->input());
if ($publicId) {
Session::flash('message', trans('texts.updated_task'));

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\TaxRateRequest;
use App\Http\Requests\CreateTaxRateRequest;
use App\Http\Requests\UpdateTaxRateRequest;
use App\Models\TaxRate;
@ -34,11 +35,12 @@ class TaxRateApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/tax_rates",
* summary="List of tax rates",
* summary="List tax rates",
* operationId="listTaxRates",
* tags={"tax_rate"},
* @SWG\Response(
* response=200,
* description="A list with tax rates",
* description="A list of tax rates",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/TaxRate"))
* ),
* @SWG\Response(
@ -56,14 +58,43 @@ class TaxRateApiController extends BaseAPIController
return $this->listResponse($taxRates);
}
/**
* @SWG\Get(
* path="/tax_rates/{tax_rate_id}",
* summary="Retrieve a tax rate",
* operationId="getTaxRate",
* tags={"tax_rate"},
* @SWG\Parameter(
* in="path",
* name="tax_rate_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single tax rate",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/TaxRate"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(TaxRateRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/tax_rates",
* tags={"tax_rate"},
* summary="Create a tax rate",
* operationId="createTaxRate",
* tags={"tax_rate"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="tax_rate",
* @SWG\Schema(ref="#/definitions/TaxRate")
* ),
* @SWG\Response(
@ -87,16 +118,23 @@ class TaxRateApiController extends BaseAPIController
/**
* @SWG\Put(
* path="/tax_rates/{tax_rate_id}",
* tags={"tax_rate"},
* summary="Update a tax rate",
* operationId="updateTaxRate",
* tags={"tax_rate"},
* @SWG\Parameter(
* in="path",
* name="tax_rate_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="tax_rate",
* @SWG\Schema(ref="#/definitions/TaxRate")
* ),
* @SWG\Response(
* response=200,
* description="Update tax rate",
* description="Updated tax rate",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/TaxRate"))
* ),
* @SWG\Response(
@ -119,4 +157,36 @@ class TaxRateApiController extends BaseAPIController
return $this->itemResponse($taxRate);
}
/**
* @SWG\Delete(
* path="/tax_rates/{tax_rate_id}",
* summary="Delete a tax rate",
* operationId="deleteTaxRate",
* tags={"tax_rate"},
* @SWG\Parameter(
* in="path",
* name="tax_rate_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted tax rate",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/TaxRate"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(UpdateTaxRateRequest $request)
{
$entity = $request->entity();
$this->taxRateRepo->delete($entity);
return $this->itemResponse($entity);
}
}

View File

@ -145,7 +145,7 @@ class TokenController extends BaseController
} else {
$token = AccountToken::createNew();
$token->name = trim(Input::get('name'));
$token->token = str_random(RANDOM_KEY_LENGTH);
$token->token = strtolower(str_random(RANDOM_KEY_LENGTH));
}
$token->save();

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\UserRequest;
use App\Http\Requests\CreateUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Models\User;
@ -25,22 +26,117 @@ class UserApiController extends BaseAPIController
$this->userRepo = $userRepo;
}
/**
* @SWG\Get(
* path="/users",
* summary="List users",
* operationId="listUsers",
* tags={"user"},
* @SWG\Response(
* response=200,
* description="A list of users",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/User"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function index()
{
$users = User::whereAccountId(Auth::user()->account_id)
->withTrashed()
->orderBy('created_at', 'desc');
return $this->listResponse($users);
}
/*
/**
* @SWG\Get(
* path="/users/{user_id}",
* summary="Retrieve a user",
* operationId="getUser",
* tags={"client"},
* @SWG\Parameter(
* in="path",
* name="user_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single user",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/User"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(UserRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/users",
* summary="Create a user",
* operationId="createUser",
* tags={"user"},
* @SWG\Parameter(
* in="body",
* name="user",
* @SWG\Schema(ref="#/definitions/User")
* ),
* @SWG\Response(
* response=200,
* description="New user",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/User"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function store(CreateUserRequest $request)
{
return $this->save($request);
}
*/
/**
* @SWG\Put(
* path="/users/{user_id}",
* summary="Update a user",
* operationId="updateUser",
* tags={"user"},
* @SWG\Parameter(
* in="path",
* name="user_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="user",
* @SWG\Schema(ref="#/definitions/User")
* ),
* @SWG\Response(
* response=200,
* description="Updated user",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/User"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*
* @param mixed $userPublicId
*/
public function update(UpdateUserRequest $request, $userPublicId)
{
$user = Auth::user();
@ -66,4 +162,36 @@ class UserApiController extends BaseAPIController
return $this->response($data);
}
/**
* @SWG\Delete(
* path="/users/{user_id}",
* summary="Delete a user",
* operationId="deleteUser",
* tags={"user"},
* @SWG\Parameter(
* in="path",
* name="user_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted user",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/User"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(UpdateUserRequest $request)
{
$entity = $request->entity();
$this->userRepo->delete($entity);
return $this->itemResponse($entity);
}
}

View File

@ -199,8 +199,8 @@ class UserController extends BaseController
$user->username = trim(Input::get('email'));
$user->email = trim(Input::get('email'));
$user->registered = true;
$user->password = str_random(RANDOM_KEY_LENGTH);
$user->confirmation_code = str_random(RANDOM_KEY_LENGTH);
$user->password = strtolower(str_random(RANDOM_KEY_LENGTH));
$user->confirmation_code = strtolower(str_random(RANDOM_KEY_LENGTH));
$user->public_id = $lastUser->public_id + 1;
if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) {
$user->is_admin = boolval(Input::get('is_admin'));
@ -210,7 +210,7 @@ class UserController extends BaseController
$user->save();
if (! $user->confirmed) {
if (! $user->confirmed && Input::get('action') === 'email') {
$this->userMailer->sendConfirmation($user, Auth::user());
$message = trans('texts.sent_invite');
} else {

View File

@ -2,10 +2,9 @@
namespace App\Http\Controllers;
// vendor
use App\Http\Requests\VendorRequest;
use App\Http\Requests\CreateVendorRequest;
use App\Http\Requests\UpdateVendorRequest;
use App\Http\Requests\VendorRequest;
use App\Models\Vendor;
use App\Ninja\Repositories\VendorRepository;
use Input;
@ -35,11 +34,12 @@ class VendorApiController extends BaseAPIController
/**
* @SWG\Get(
* path="/vendors",
* summary="List of vendors",
* summary="List vendors",
* operationId="listVendors",
* tags={"vendor"},
* @SWG\Response(
* response=200,
* description="A list with vendors",
* description="A list of vendors",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Vendor"))
* ),
* @SWG\Response(
@ -57,14 +57,43 @@ class VendorApiController extends BaseAPIController
return $this->listResponse($vendors);
}
/**
* @SWG\Get(
* path="/vendors/{vendor_id}",
* summary="Retrieve a vendor",
* operationId="getVendor",
* tags={"client"},
* @SWG\Parameter(
* in="path",
* name="vendor_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single vendor",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(VendorRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/vendors",
* tags={"vendor"},
* summary="Create a vendor",
* operationId="createVendor",
* tags={"vendor"},
* @SWG\Parameter(
* in="body",
* name="body",
* name="vendor",
* @SWG\Schema(ref="#/definitions/Vendor")
* ),
* @SWG\Response(
@ -92,16 +121,23 @@ class VendorApiController extends BaseAPIController
/**
* @SWG\Put(
* path="/vendors/{vendor_id}",
* tags={"vendor"},
* summary="Update a vendor",
* operationId="updateVendor",
* tags={"vendor"},
* @SWG\Parameter(
* in="path",
* name="vendor_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="body",
* name="vendor",
* @SWG\Schema(ref="#/definitions/Vendor")
* ),
* @SWG\Response(
* response=200,
* description="Update vendor",
* description="Updated vendor",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor"))
* ),
* @SWG\Response(
@ -130,16 +166,18 @@ class VendorApiController extends BaseAPIController
/**
* @SWG\Delete(
* path="/vendors/{vendor_id}",
* tags={"vendor"},
* summary="Delete a vendor",
* operationId="deleteVendor",
* tags={"vendor"},
* @SWG\Parameter(
* in="body",
* name="body",
* @SWG\Schema(ref="#/definitions/Vendor")
* in="path",
* name="vendor_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Delete vendor",
* description="Deleted vendor",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor"))
* ),
* @SWG\Response(
@ -148,7 +186,7 @@ class VendorApiController extends BaseAPIController
* )
* )
*/
public function destroy(VendorRequest $request)
public function destroy(UpdateVendorRequest $request)
{
$vendor = $request->entity();

View File

@ -1,7 +1,7 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
@ -34,6 +34,6 @@ class Kernel extends HttpKernel
'permissions.required' => 'App\Http\Middleware\PermissionsRequired',
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
'api' => 'App\Http\Middleware\ApiCheck',
'cors' => '\App\Http\Middleware\Cors',
'cors' => '\Barryvdh\Cors\HandleCors',
];
}

View File

@ -56,12 +56,14 @@ class Authenticate
$contact_key = session('contact_key');
}
$contact = false;
if ($contact_key) {
$contact = $this->getContact($contact_key);
} elseif ($invitation = $this->getInvitation($request->invitation_key)) {
$contact = $invitation->contact;
Session::put('contact_key', $contact->contact_key);
} else {
}
if (! $contact) {
return \Redirect::to('client/sessionexpired');
}
$account = $contact->account;
@ -113,6 +115,7 @@ class Authenticate
// check for extra params at end of value (from website feature)
list($key) = explode('&', $key);
$key = substr($key, 0, RANDOM_KEY_LENGTH);
$invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first();
if ($invitation && ! $invitation->is_deleted) {

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class ContactRequest extends EntityRequest
{
protected $entityType = ENTITY_CONTACT;
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
class CreateContactRequest extends ContactRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', ENTITY_CONTACT);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required',
'client_id' => 'required',
];
}
}

View File

@ -26,7 +26,7 @@ class CreatePaymentAPIRequest extends PaymentRequest
if (! $this->invoice_id || ! $this->amount) {
return [
'invoice_id' => 'required|numeric|min:1',
'amount' => 'required|numeric|min:0.01',
'amount' => 'required|numeric|not_in:0',
];
}

View File

@ -28,10 +28,15 @@ class CreatePaymentRequest extends PaymentRequest
->invoices()
->firstOrFail();
$this->merge([
'invoice_id' => $invoice->id,
'client_id' => $invoice->client->id,
]);
$rules = [
'client' => 'required', // TODO: change to client_id once views are updated
'invoice' => 'required', // TODO: change to invoice_id once views are updated
'amount' => "required|numeric|between:0.01,{$invoice->balance}",
'amount' => 'required|numeric|not_in:0',
'payment_date' => 'required',
];

View File

@ -3,6 +3,7 @@
namespace App\Http\Requests;
use App\Models\ExpenseCategory;
use App\Models\Vendor;
class ExpenseRequest extends EntityRequest
{
@ -24,11 +25,37 @@ class ExpenseRequest extends EntityRequest
{
$input = $this->all();
if ($this->expense_category_id) {
// check if we're creating a new expense category
if ($this->expense_category_id == '-1') {
$data = [
'name' => trim($this->expense_category_name)
];
if (ExpenseCategory::validate($data) === true) {
$category = app('App\Ninja\Repositories\ExpenseCategoryRepository')->save($data);
$input['expense_category_id'] = $category->id;
} else {
$input['expense_category_id'] = null;
}
} elseif ($this->expense_category_id) {
$input['expense_category_id'] = ExpenseCategory::getPrivateId($this->expense_category_id);
$this->replace($input);
}
// check if we're creating a new vendor
if ($this->vendor_id == '-1') {
$data = [
'name' => trim($this->vendor_name)
];
if (Vendor::validate($data) === true) {
$vendor = app('App\Ninja\Repositories\VendorRepository')->save($data);
// TODO change to private id once service is refactored
$input['vendor_id'] = $vendor->public_id;
} else {
$input['vendor_id'] = null;
}
}
$this->replace($input);
return $this->all();
}
}

View File

@ -53,7 +53,7 @@ class SaveClientPortalSettings extends Request
$input['subdomain'] = null;
}
}
$this->replace($input);
return $this->all();

View File

@ -23,6 +23,7 @@ class SaveEmailSettings extends Request
{
return [
'bcc_email' => 'email',
'reply_to_email' => 'email',
];
}
}

View File

@ -2,7 +2,33 @@
namespace App\Http\Requests;
use App\Models\Client;
use App\Models\Project;
class TaskRequest extends EntityRequest
{
protected $entityType = ENTITY_TASK;
public function sanitize()
{
$input = $this->all();
// check if we're creating a new project
if ($this->project_id == '-1') {
$project = [
'name' => trim($this->project_name),
'client_id' => Client::getPrivateId($this->client),
];
if (Project::validate($project) === true) {
$project = app('App\Ninja\Repositories\ProjectRepository')->save($project);
$input['project_id'] = $project->public_id;
} else {
$input['project_id'] = null;
}
}
$this->replace($input);
return $this->all();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
class UpdateContactRequest extends ContactRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required',
];
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class UserRequest extends EntityRequest
{
protected $entityType = ENTITY_USER;
}

View File

@ -113,6 +113,10 @@ if (Utils::isReseller()) {
Route::post('/reseller_stats', 'AppController@stats');
}
if (Utils::isTravis()) {
Route::get('/check_data', 'AppController@checkData');
}
Route::group(['middleware' => 'auth:user'], function () {
Route::get('dashboard', 'DashboardController@index');
Route::get('dashboard_chart_data/{group_by}/{start_date}/{end_date}/{currency_id}/{include_expenses}', 'DashboardController@chartData');
@ -126,7 +130,7 @@ Route::group(['middleware' => 'auth:user'], function () {
Route::get('settings/user_details', 'AccountController@showUserDetails');
Route::post('settings/user_details', 'AccountController@saveUserDetails');
Route::post('settings/payment_gateway_limits', 'AccountController@savePaymentGatewayLimits');
Route::post('settings/payment_gateway_limits', 'AccountGatewayController@savePaymentGatewayLimits');
Route::post('users/change_password', 'UserController@changePassword');
Route::resource('clients', 'ClientController');
@ -253,6 +257,7 @@ Route::group([
Route::post('settings/change_plan', 'AccountController@changePlan');
Route::post('settings/cancel_account', 'AccountController@cancelAccount');
Route::post('settings/purge_data', 'AccountController@purgeData');
Route::post('settings/company_details', 'AccountController@updateDetails');
Route::post('settings/{section?}', 'AccountController@doSection');
@ -303,12 +308,11 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function () {
Route::get('accounts', 'AccountApiController@show');
Route::put('accounts', 'AccountApiController@update');
Route::resource('clients', 'ClientApiController');
Route::resource('contacts', 'ContactApiController');
Route::get('quotes', 'QuoteApiController@index');
Route::get('invoices', 'InvoiceApiController@index');
Route::get('download/{invoice_id}', 'InvoiceApiController@download');
Route::resource('invoices', 'InvoiceApiController');
Route::resource('payments', 'PaymentApiController');
Route::get('tasks', 'TaskApiController@index');
Route::resource('tasks', 'TaskApiController');
Route::post('hooks', 'IntegrationController@subscribe');
Route::post('email_invoice', 'InvoiceApiController@emailInvoice');
@ -359,6 +363,9 @@ Route::get('/feed', function () {
Route::get('/comments/feed', function () {
return Redirect::to(NINJA_WEB_URL.'/comments/feed', 301);
});
Route::get('/terms', function () {
return Redirect::to(NINJA_WEB_URL.'/terms', 301);
});
/*
if (Utils::isNinjaDev())

80
app/Jobs/ImportData.php Normal file
View File

@ -0,0 +1,80 @@
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Monolog\Logger;
use App\Services\ImportService;
use App\Ninja\Mailers\UserMailer;
use Auth;
/**
* Class SendInvoiceEmail.
*/
class ImportData extends Job implements ShouldQueue
{
use InteractsWithQueue, SerializesModels;
/**
* @var User
*/
protected $user;
/**
* @var string
*/
protected $type;
/**
* @var array
*/
protected $settings;
/**
* Create a new job instance.
*
* @param mixed $files
* @param mixed $settings
*/
public function __construct($user, $type, $settings)
{
$this->user = $user;
$this->type = $type;
$this->settings = $settings;
}
/**
* Execute the job.
*
* @param ContactMailer $mailer
*/
public function handle(ImportService $importService, UserMailer $userMailer)
{
$includeSettings = false;
Auth::onceUsingId($this->user->id);
$this->user->account->loadLocalizationSettings();
if ($this->type === IMPORT_JSON) {
$includeData = $this->settings['include_data'];
$includeSettings = $this->settings['include_settings'];
$files = $this->settings['files'];
$results = $importService->importJSON($files[IMPORT_JSON], $includeData, $includeSettings);
} elseif ($this->type === IMPORT_CSV) {
$map = $this->settings['map'];
$headers = $this->settings['headers'];
$timestamp = $this->settings['timestamp'];
$results = $importService->importCSV($map, $headers, $timestamp);
} else {
$source = $this->settings['source'];
$files = $this->settings['files'];
$results = $importService->importFiles($source, $files);
}
$subject = trans('texts.import_complete');
$message = $importService->presentResults($results, $includeSettings);
$userMailer->sendMessage($this->user, $subject, $message);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Jobs;
use App\Jobs\Job;
use App\Models\Document;
use Auth;
use DB;
use Exception;
class PurgeAccountData extends Job
{
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$user = Auth::user();
$account = $user->account;
if (! $user->is_admin) {
throw new Exception(trans('texts.forbidden'));
}
// delete the documents from cloud storage
Document::scope()->each(function ($item, $key) {
$item->delete();
});
$tables = [
'activities',
'invitations',
'account_gateway_tokens',
'payment_methods',
'credits',
'expense_categories',
'expenses',
'invoice_items',
'payments',
'invoices',
'tasks',
'projects',
'products',
'vendor_contacts',
'vendors',
'contacts',
'clients',
];
foreach ($tables as $table) {
DB::table($table)->where('account_id', '=', $user->account_id)->delete();
}
$account->invoice_number_counter = 1;
$account->quote_number_counter = 1;
$account->client_number_counter = 1;
$account->save();
}
}

View File

@ -26,11 +26,6 @@ class SendInvoiceEmail extends Job implements ShouldQueue
*/
protected $reminder;
/**
* @var string
*/
protected $pdfString;
/**
* @var array
*/
@ -44,11 +39,10 @@ class SendInvoiceEmail extends Job implements ShouldQueue
* @param bool $reminder
* @param mixed $pdfString
*/
public function __construct(Invoice $invoice, $reminder = false, $pdfString = false, $template = false)
public function __construct(Invoice $invoice, $reminder = false, $template = false)
{
$this->invoice = $invoice;
$this->reminder = $reminder;
$this->pdfString = $pdfString;
$this->template = $template;
}
@ -59,7 +53,7 @@ class SendInvoiceEmail extends Job implements ShouldQueue
*/
public function handle(ContactMailer $mailer)
{
$mailer->sendInvoice($this->invoice, $this->reminder, $this->pdfString, $this->template);
$mailer->sendInvoice($this->invoice, $this->reminder, $this->template);
}
/*

View File

@ -394,6 +394,7 @@ class Utils
'user_name' => Auth::check() ? Auth::user()->getDisplayName() : '',
'method' => Request::method(),
'url' => Input::get('url', Request::url()),
'previous' => url()->previous(),
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'ip' => Request::getClientIp(),
'count' => Session::get('error_count', 0),

View File

@ -5,7 +5,9 @@ namespace App\Listeners;
use App\Events\UserLoggedIn;
use App\Events\UserSignedUp;
use App\Libraries\HistoryUtils;
use App\Models\Gateway;
use App\Ninja\Repositories\AccountRepository;
use Utils;
use Auth;
use Carbon;
use Session;
@ -65,5 +67,10 @@ class HandleUserLoggedIn
} elseif ($account->isLogoTooLarge()) {
Session::flash('warning', trans('texts.logo_too_large', ['size' => $account->getLogoSize() . 'KB']));
}
// check custom gateway id is correct
if (! Utils::isNinja() && Gateway::find(GATEWAY_CUSTOM)->name !== 'Custom') {
Session::flash('error', trans('texts.error_incorrect_gateway_ids'));
}
}
}

View File

@ -153,6 +153,14 @@ class InvoiceListener
public function jobFailed(JobExceptionOccurred $exception)
{
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw(print_r($exception->data, true), function ($message) use ($errorEmail) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject('Job failed');
});
}
Utils::logError($exception->exception);
}
}

View File

@ -48,51 +48,131 @@ class Account extends Eloquent
* @var array
*/
protected $fillable = [
'timezone_id',
'date_format_id',
'datetime_format_id',
'currency_id',
'name',
'id_number',
'vat_number',
'work_email',
'website',
'work_phone',
'address1',
'address2',
'city',
'state',
'postal_code',
'country_id',
'size_id',
'industry_id',
'invoice_terms',
'email_footer',
'timezone_id',
'date_format_id',
'datetime_format_id',
'currency_id',
'language_id',
'military_time',
'industry_id',
'size_id',
'invoice_taxes',
'invoice_item_taxes',
'invoice_design_id',
'work_phone',
'work_email',
'language_id',
'custom_label1',
'custom_value1',
'custom_label2',
'custom_value2',
'custom_client_label1',
'custom_client_label2',
'fill_products',
'update_products',
'primary_color',
'secondary_color',
'hide_quantity',
'hide_paid_to_date',
'custom_invoice_label1',
'custom_invoice_label2',
'custom_invoice_taxes1',
'custom_invoice_taxes2',
'vat_number',
'invoice_number_prefix',
'invoice_number_counter',
'quote_number_prefix',
'quote_number_counter',
'share_counter',
'id_number',
'email_template_invoice',
'email_template_quote',
'email_template_payment',
'token_billing_type_id',
'invoice_footer',
'pdf_email_attachment',
'font_size',
'invoice_labels',
'custom_design',
'show_item_taxes',
'military_time',
'email_subject_invoice',
'email_subject_quote',
'email_subject_payment',
'email_subject_reminder1',
'email_subject_reminder2',
'email_subject_reminder3',
'email_template_reminder1',
'email_template_reminder2',
'email_template_reminder3',
'enable_reminder1',
'enable_reminder2',
'enable_reminder3',
'num_days_reminder1',
'num_days_reminder2',
'num_days_reminder3',
'custom_invoice_text_label1',
'custom_invoice_text_label2',
'default_tax_rate_id',
'enable_second_tax_rate',
'include_item_taxes_inline',
'start_of_week',
'financial_year_start',
'enable_client_portal',
'enable_client_portal_dashboard',
'recurring_hour',
'invoice_number_pattern',
'quote_number_pattern',
'quote_terms',
'email_design_id',
'enable_email_markup',
'website',
'direction_reminder1',
'direction_reminder2',
'direction_reminder3',
'field_reminder1',
'field_reminder2',
'field_reminder3',
'header_font_id',
'body_font_id',
'auto_convert_quote',
'all_pages_footer',
'all_pages_header',
'show_currency_code',
'enable_portal_password',
'send_portal_password',
'custom_invoice_item_label1',
'custom_invoice_item_label2',
'recurring_invoice_number_prefix',
'enable_client_portal',
'invoice_fields',
'invoice_embed_documents',
'document_email_attachment',
'enable_client_portal_dashboard',
'page_size',
'live_preview',
'invoice_number_padding',
'enable_second_tax_rate',
'auto_bill_on_due_date',
'start_of_week',
'enable_buy_now_buttons',
'include_item_taxes_inline',
'financial_year_start',
'enabled_modules',
'enabled_dashboard_sections',
'show_accept_invoice_terms',
'show_accept_quote_terms',
'require_invoice_signature',
'require_quote_signature',
'pdf_email_attachment',
'document_email_attachment',
'email_design_id',
'enable_email_markup',
'domain_id',
'client_number_prefix',
'client_number_counter',
'client_number_pattern',
'payment_terms',
'reset_counter_frequency_id',
'payment_type_id',
'gateway_fee_enabled',
'reset_counter_date',
];
/**
@ -189,6 +269,22 @@ class Account extends Eloquent
return $this->hasMany('App\Models\AccountGateway');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function account_gateway_settings()
{
return $this->hasMany('App\Models\AccountGatewaySettings');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function account_email_settings()
{
return $this->hasOne('App\Models\AccountEmailSettings');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -402,6 +498,22 @@ class Account extends Eloquent
return $user->getDisplayName();
}
public function getGatewaySettings($gatewayTypeId)
{
if (! $this->relationLoaded('account_gateway_settings')) {
$this->load('account_gateway_settings');
}
foreach ($this->account_gateway_settings as $settings) {
if ($settings->gateway_type_id == $gatewayTypeId) {
return $settings;
}
}
return false;
}
/**
* @return string
*/
@ -887,6 +999,8 @@ class Account extends Eloquent
$invoice->start_date = Utils::today();
$invoice->invoice_design_id = $this->invoice_design_id;
$invoice->client_id = $clientId;
$invoice->custom_taxes1 = $this->custom_invoice_taxes1;
$invoice->custom_taxes2 = $this->custom_invoice_taxes2;
if ($entityType === ENTITY_RECURRING_INVOICE) {
$invoice->invoice_number = microtime(true);

View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Eloquent;
/**
* Class Account.
*/
class AccountEmailSettings extends Eloquent
{
/**
* @var array
*/
protected $fillable = [
'bcc_email',
'reply_to_email',
'email_subject_invoice',
'email_subject_quote',
'email_subject_payment',
'email_template_invoice',
'email_template_quote',
'email_template_payment',
'email_subject_reminder1',
'email_subject_reminder2',
'email_subject_reminder3',
'email_template_reminder1',
'email_template_reminder2',
'email_template_reminder3',
];
}

View File

@ -2,6 +2,8 @@
namespace App\Models;
use Utils;
/**
* Class AccountGatewaySettings.
*/
@ -12,6 +14,18 @@ class AccountGatewaySettings extends EntityModel
*/
protected $dates = ['updated_at'];
/**
* @var array
*/
protected $fillable = [
'fee_amount',
'fee_percent',
'fee_tax_name1',
'fee_tax_rate1',
'fee_tax_name2',
'fee_tax_rate2',
];
/**
* @var bool
*/
@ -29,4 +43,29 @@ class AccountGatewaySettings extends EntityModel
{
// to Disable created_at
}
public function areFeesEnabled()
{
return floatval($this->fee_amount) || floatval($this->fee_percent);
}
public function hasTaxes()
{
return floatval($this->fee_tax_rate1) || floatval($this->fee_tax_rate1);
}
public function feesToString()
{
$parts = [];
if (floatval($this->fee_amount) != 0) {
$parts[] = Utils::formatMoney($this->fee_amount);
}
if (floatval($this->fee_percent) != 0) {
$parts[] = (floor($this->fee_percent * 1000) / 1000) . '%';
}
return join(' + ', $parts);
}
}

View File

@ -49,6 +49,8 @@ class Client extends EntityModel
'language_id',
'payment_terms',
'website',
'invoice_number_counter',
'quote_number_counter',
];
/**
@ -136,7 +138,7 @@ class Client extends EntityModel
'email' => 'email',
'mobile|phone' => 'phone',
'name|organization' => 'name',
'street2|address2' => 'address2',
'apt|street2|address2' => 'address2',
'street|address|address1' => 'address1',
'city' => 'city',
'state|province' => 'state',
@ -145,7 +147,7 @@ class Client extends EntityModel
'note' => 'notes',
'site|website' => 'website',
'vat' => 'vat_number',
'id|number' => 'id_number',
'number' => 'id_number',
];
}
@ -288,7 +290,7 @@ class Client extends EntityModel
if (isset($data['contact_key']) && $this->account->account_key == env('NINJA_LICENSE_ACCOUNT_KEY')) {
$contact->contact_key = $data['contact_key'];
} else {
$contact->contact_key = str_random(RANDOM_KEY_LENGTH);
$contact->contact_key = strtolower(str_random(RANDOM_KEY_LENGTH));
}
}

View File

@ -145,4 +145,32 @@ class Company extends Eloquent
return false;
}
public function processRefund($user)
{
if (! $this->payment) {
return false;
}
$account = $this->accounts()->first();
$planDetails = $account->getPlanDetails(false, false);
if (! empty($planDetails['started'])) {
$deadline = clone $planDetails['started'];
$deadline->modify('+30 days');
if ($deadline >= date_create()) {
$accountRepo = app('App\Ninja\Repositories\AccountRepository');
$ninjaAccount = $accountRepo->getNinjaAccount();
$paymentDriver = $ninjaAccount->paymentDriver();
$paymentDriver->refundPayment($this->payment);
\Log::info("Refunded Plan Payment: {$account->name} - {$user->email} - Deadline: {$deadline->format('Y-m-d')}");
return true;
}
}
return false;
}
}

View File

@ -119,7 +119,7 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
public function getContactKeyAttribute($contact_key)
{
if (empty($contact_key) && $this->id) {
$this->contact_key = $contact_key = str_random(RANDOM_KEY_LENGTH);
$this->contact_key = $contact_key = strtolower(str_random(RANDOM_KEY_LENGTH));
static::where('id', $this->id)->update(['contact_key' => $contact_key]);
}

View File

@ -23,6 +23,14 @@ class Credit extends EntityModel
*/
protected $presenter = 'App\Ninja\Presenters\CreditPresenter';
/**
* @var array
*/
protected $fillable = [
'public_notes',
'private_notes',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Str;
use Auth;
use Eloquent;
use Utils;
@ -95,6 +96,10 @@ class EntityModel extends Eloquent
*/
public static function getPrivateId($publicId)
{
if (! $publicId) {
return null;
}
$className = get_called_class();
return $className::scope($publicId)->withTrashed()->value('id');
@ -254,16 +259,22 @@ class EntityModel extends Eloquent
* @param $data
* @param $entityType
* @param mixed $entity
*
* TODO Remove $entityType parameter
* @return bool|string
*/
public static function validate($data, $entityType, $entity = false)
public static function validate($data, $entityType = false, $entity = false)
{
if (! $entityType) {
$className = get_called_class();
$entityBlank = new $className();
$entityType = $entityBlank->getEntityType();
}
// Use the API request if it exists
$action = $entity ? 'update' : 'create';
$requestClass = sprintf('App\\Http\\Requests\\%s%sAPIRequest', ucwords($action), ucwords($entityType));
$requestClass = sprintf('App\\Http\\Requests\\%s%sAPIRequest', ucwords($action), Str::studly($entityType));
if (! class_exists($requestClass)) {
$requestClass = sprintf('App\\Http\\Requests\\%s%sRequest', ucwords($action), ucwords($entityType));
$requestClass = sprintf('App\\Http\\Requests\\%s%sRequest', ucwords($action), Str::studly($entityType));
}
$request = new $requestClass();

View File

@ -59,6 +59,7 @@ class Gateway extends Eloquent
GATEWAY_PAYPAL_EXPRESS,
GATEWAY_BITPAY,
GATEWAY_DWOLLA,
GATEWAY_CUSTOM,
];
/**

View File

@ -76,6 +76,10 @@ class Invitation extends EntityModel
$iframe_url = $account->iframe_url;
$url = trim(SITE_URL, '/');
if (env('REQUIRE_HTTPS')) {
$url = str_replace('http://', 'https://', $url);
}
if ($account->hasFeature(FEATURE_CUSTOM_URL)) {
if (Utils::isNinjaProd()) {
$url = $account->present()->clientPortalLink();

View File

@ -10,6 +10,7 @@ use App\Events\QuoteWasCreated;
use App\Events\QuoteWasUpdated;
use App\Libraries\CurlUtils;
use App\Models\Activity;
use App\Models\Traits\ChargesFees;
use DateTime;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait;
@ -23,6 +24,7 @@ class Invoice extends EntityModel implements BalanceAffecting
{
use PresentableTrait;
use OwnedByClientTrait;
use ChargesFees;
use SoftDeletes {
SoftDeletes::trashed as parentTrashed;
}
@ -62,9 +64,10 @@ class Invoice extends EntityModel implements BalanceAffecting
*/
public static $patternFields = [
'counter',
'custom1',
'custom2',
'idNumber',
'clientCounter',
'clientIdNumber',
'clientCustom1',
'clientCustom2',
'userId',
'year',
'date:',
@ -539,7 +542,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function updatePaidStatus($save = true)
{
$statusId = false;
if ($this->amount > 0 && $this->balance == 0) {
if ($this->amount != 0 && $this->balance == 0) {
$statusId = INVOICE_STATUS_PAID;
} elseif ($this->balance > 0 && $this->balance < $this->amount) {
$statusId = INVOICE_STATUS_PARTIAL;
@ -573,6 +576,13 @@ class Invoice extends EntityModel implements BalanceAffecting
return;
}
$balanceAdjustment = floatval($balanceAdjustment);
$partial = floatval($partial);
if (! $balanceAdjustment && $this->partial == $partial) {
return;
}
$this->balance = $this->balance + $balanceAdjustment;
if ($this->partial > 0) {
@ -580,6 +590,13 @@ class Invoice extends EntityModel implements BalanceAffecting
}
$this->save();
// mark fees as paid
if ($balanceAdjustment != 0 && $this->account->gateway_fee_enabled) {
if ($invoiceItem = $this->getGatewayFeeItem()) {
$invoiceItem->markFeePaid();
}
}
}
/**
@ -610,7 +627,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function canBePaid()
{
return floatval($this->balance) > 0 && ! $this->is_deleted && $this->isInvoice();
return floatval($this->balance) != 0 && ! $this->is_deleted && $this->isInvoice();
}
public static function calcStatusLabel($status, $class, $entityType, $quoteInvoiceId)
@ -752,7 +769,16 @@ class Invoice extends EntityModel implements BalanceAffecting
*/
public function getRequestedAmount()
{
return $this->partial > 0 ? $this->partial : $this->balance;
$fee = 0;
if ($this->account->gateway_fee_enabled) {
$fee = $this->getGatewayFee();
}
if ($this->partial > 0) {
return $this->partial + $fee;
} else {
return $this->balance;
}
}
/**
@ -868,6 +894,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'page_size',
'include_item_taxes_inline',
'invoice_fields',
'show_currency_code',
]);
foreach ($this->invoice_items as $invoiceItem) {
@ -1231,6 +1258,10 @@ class Invoice extends EntityModel implements BalanceAffecting
return false;
}
if (Utils::isTravis()) {
return false;
}
$invitation = $this->invitations[0];
$link = $invitation->getLink('view', true);
$pdfString = false;
@ -1238,26 +1269,35 @@ class Invoice extends EntityModel implements BalanceAffecting
try {
if (env('PHANTOMJS_BIN_PATH')) {
$pdfString = CurlUtils::phantom('GET', $link . '?phantomjs=true&phantomjs_secret=' . env('PHANTOMJS_SECRET'));
} elseif ($key = env('PHANTOMJS_CLOUD_KEY')) {
if (Utils::isNinjaDev()) {
$link = env('TEST_LINK');
}
if (! $pdfString && (Utils::isNinja() || ! env('PHANTOMJS_BIN_PATH'))) {
if ($key = env('PHANTOMJS_CLOUD_KEY')) {
if (Utils::isNinjaDev()) {
$link = env('TEST_LINK');
}
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D";
$pdfString = CurlUtils::get($url);
}
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D";
$pdfString = CurlUtils::get($url);
}
$pdfString = strip_tags($pdfString);
} catch (\Exception $exception) {
Utils::logError("PhantomJS - Failed to create pdf: {$exception->getMessage()}");
Utils::logError("PhantomJS - Failed to load: {$exception->getMessage()}");
return false;
}
if (! $pdfString || strlen($pdfString) < 200) {
Utils::logError("PhantomJS - Failed to create pdf: {$pdfString}");
Utils::logError("PhantomJS - Invalid response: {$pdfString}");
return false;
}
return Utils::decodePDF($pdfString);
if ($pdf = Utils::decodePDF($pdfString)) {
return $pdf;
} else {
Utils::logError("PhantomJS - Unable to decode: {$pdfString}");
return false;
}
}
/**
@ -1473,7 +1513,7 @@ class Invoice extends EntityModel implements BalanceAffecting
}
Invoice::creating(function ($invoice) {
if (! $invoice->is_recurring) {
if (! $invoice->is_recurring && $invoice->amount >= 0) {
$invoice->account->incrementCounter($invoice);
}
});

View File

@ -39,6 +39,7 @@ class InvoiceItem extends EntityModel
'tax_rate1',
'tax_name2',
'tax_rate2',
'invoice_item_type_id',
];
/**
@ -72,4 +73,28 @@ class InvoiceItem extends EntityModel
{
return $this->belongsTo('App\Models\Account');
}
public function amount()
{
$amount = $this->cost * $this->qty;
$preTaxAmount = $amount;
if ($this->tax_rate1) {
$amount += $preTaxAmount * $this->tax_rate1 / 100;
}
if ($this->tax_rate2) {
$amount += $preTaxAmount * $this->tax_rate2 / 100;
}
return $amount;
}
public function markFeePaid()
{
if ($this->invoice_item_type_id == INVOICE_ITEM_TYPE_PENDING_GATEWAY_FEE) {
$this->invoice_item_type_id = INVOICE_ITEM_TYPE_PAID_GATEWAY_FEE;
$this->save();
}
}
}

View File

@ -205,7 +205,7 @@ class Task extends EntityModel
public function scopeDateRange($query, $startDate, $endDate)
{
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) >= ' . $startDate->format('U'));
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) <= ' . $endDate->format('U'));
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) <= ' . $endDate->modify('+1 day')->format('U'));
return $query;
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Models\Traits;
use App\Models\GatewayType;
use App\Models\InvoiceItem;
use App\Models\AccountGatewaySettings;
/**
* Class ChargesFees
*/
trait ChargesFees
{
public function calcGatewayFee($gatewayTypeId = false, $includeTax = false)
{
$account = $this->account;
$settings = $account->getGatewaySettings($gatewayTypeId);
$fee = 0;
if (! $account->gateway_fee_enabled) {
return false;
}
if ($settings->fee_amount) {
$fee += $settings->fee_amount;
}
if ($settings->fee_percent) {
$amount = $this->partial > 0 ? $this->partial : $this->balance;
$fee += $amount * $settings->fee_percent / 100;
}
// calculate final amount with tax
if ($includeTax) {
$preTaxFee = $fee;
if ($settings->fee_tax_rate1) {
$fee += $preTaxFee * $settings->fee_tax_rate1 / 100;
}
if ($settings->fee_tax_rate2) {
$fee += $preTaxFee * $settings->fee_tax_rate2 / 100;
}
}
return round($fee, 2);
}
public function getGatewayFee()
{
$account = $this->account;
if (! $account->gateway_fee_enabled) {
return 0;
}
$item = $this->getGatewayFeeItem();
return $item ? $item->amount() : 0;
}
public function getGatewayFeeItem()
{
if (! $this->relationLoaded('invoice_items')) {
$this->load('invoice_items');
}
foreach ($this->invoice_items as $item) {
if ($item->invoice_item_type_id == INVOICE_ITEM_TYPE_PENDING_GATEWAY_FEE) {
return $item;
}
}
return false;
}
}

View File

@ -39,6 +39,10 @@ trait GeneratesNumbers
$number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT);
}
if ($entity->recurring_invoice_id) {
$number = $this->recurring_invoice_number_prefix . $number;
}
if ($entity->isEntityType(ENTITY_CLIENT)) {
$check = Client::scope(false, $this->id)->whereIdNumber($number)->withTrashed()->first();
} else {
@ -66,10 +70,6 @@ trait GeneratesNumbers
}
}
if ($entity->recurring_invoice_id) {
$number = $this->recurring_invoice_number_prefix . $number;
}
return $number;
}
@ -125,7 +125,7 @@ trait GeneratesNumbers
{
$pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern;
return strstr($pattern, '$custom') || strstr($pattern, '$idNumber');
return strstr($pattern, '$client') !== false || strstr($pattern, '$idNumber') !== false;
}
/**
@ -167,10 +167,7 @@ trait GeneratesNumbers
}
$pattern = str_replace($search, $replace, $pattern);
if ($entity->client_id) {
$pattern = $this->getClientInvoiceNumber($pattern, $entity);
}
$pattern = $this->getClientInvoiceNumber($pattern, $entity);
return $pattern;
}
@ -183,7 +180,7 @@ trait GeneratesNumbers
*/
private function getClientInvoiceNumber($pattern, $invoice)
{
if (! $invoice->client) {
if (! $invoice->client_id) {
return $pattern;
}
@ -191,12 +188,21 @@ trait GeneratesNumbers
'{$custom1}',
'{$custom2}',
'{$idNumber}',
'{$clientCustom1}',
'{$clientCustom2}',
'{$clientIdNumber}',
'{$clientCounter}',
];
$replace = [
$invoice->client->custom_value1,
$invoice->client->custom_value2,
$invoice->client->id_number,
$invoice->client->custom_value1, // backwards compatibility
$invoice->client->custom_value2,
$invoice->client->id_number,
str_pad($invoice->client->invoice_number_counter, $this->invoice_number_padding, '0', STR_PAD_LEFT),
str_pad($invoice->client->quote_number_counter, $this->invoice_number_padding, '0', STR_PAD_LEFT),
];
return str_replace($search, $replace, $pattern);
@ -225,7 +231,9 @@ trait GeneratesNumbers
*/
public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE)
{
$invoice = $this->createInvoice($entityType);
$client = \App\Models\Client::scope()->first();
$invoice = $this->createInvoice($entityType, $client ? $client->id : 0);
return $this->getNextNumber($invoice);
}
@ -239,17 +247,87 @@ trait GeneratesNumbers
if ($this->client_number_counter) {
$this->client_number_counter += 1;
}
} elseif ($entity->isType(INVOICE_TYPE_QUOTE) && ! $this->share_counter) {
$this->quote_number_counter += 1;
} else {
$this->invoice_number_counter += 1;
$this->save();
return;
}
$this->save();
if ($this->usesClientInvoiceCounter()) {
if ($entity->isType(INVOICE_TYPE_QUOTE) && ! $this->share_counter) {
$entity->client->quote_number_counter += 1;
} else {
$entity->client->invoice_number_counter += 1;
}
$entity->client->save();
}
if ($this->usesInvoiceCounter()) {
if ($entity->isType(INVOICE_TYPE_QUOTE) && ! $this->share_counter) {
$this->quote_number_counter += 1;
} else {
$this->invoice_number_counter += 1;
}
$this->save();
}
}
public function usesInvoiceCounter()
{
return strpos($this->invoice_number_pattern, '{$counter}') !== false;
}
public function usesClientInvoiceCounter()
{
return strpos($this->invoice_number_pattern, '{$clientCounter}') !== false;
}
public function clientNumbersEnabled()
{
return $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $this->client_number_counter;
return $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $this->client_number_counter > 0;
}
public function checkCounterReset()
{
if (! $this->reset_counter_frequency_id || ! $this->reset_counter_date) {
return false;
}
$timezone = $this->getTimezone();
$resetDate = Carbon::parse($this->reset_counter_date, $timezone);
if (! $resetDate->isToday()) {
return false;
}
switch ($this->reset_counter_frequency_id) {
case FREQUENCY_WEEKLY:
$resetDate->addWeek();
break;
case FREQUENCY_TWO_WEEKS:
$resetDate->addWeeks(2);
break;
case FREQUENCY_FOUR_WEEKS:
$resetDate->addWeeks(4);
break;
case FREQUENCY_MONTHLY:
$resetDate->addMonth();
break;
case FREQUENCY_TWO_MONTHS:
$resetDate->addMonths(2);
break;
case FREQUENCY_THREE_MONTHS:
$resetDate->addMonths(3);
break;
case FREQUENCY_SIX_MONTHS:
$resetDate->addMonths(6);
break;
case FREQUENCY_ANNUALLY:
$resetDate->addYear();
break;
}
$this->reset_counter_date = $resetDate->format('Y-m-d');
$this->invoice_number_counter = 1;
$this->quote_number_counter = 1;
$this->save();
}
}

View File

@ -96,14 +96,16 @@ trait PresentsInvoice
'client.client_name',
'client.id_number',
'client.vat_number',
'client.website',
'client.work_phone',
'client.address1',
'client.address2',
'client.city_state_postal',
'client.postal_city_state',
'client.country',
'client.contact_name',
'client.email',
'client.phone',
'client.contact_name',
'client.custom_value1',
'client.custom_value2',
'.blank',
@ -138,6 +140,8 @@ trait PresentsInvoice
list($entityType, $fieldName) = explode('.', $field);
if (substr($fieldName, 0, 6) == 'custom') {
$fields[$section][$field] = $labels[$field];
} elseif (in_array($field, ['client.phone', 'client.email'])) {
$fields[$section][$field] = trans('texts.contact_' . $fieldName);
} else {
$fields[$section][$field] = $labels[$fieldName];
}
@ -208,7 +212,7 @@ trait PresentsInvoice
'website',
'phone',
'blank',
'adjustment',
'surcharge',
'tax_invoice',
'tax_quote',
'statement',
@ -216,6 +220,13 @@ trait PresentsInvoice
'your_statement',
'statement_issued_to',
'statement_to',
'credit_note',
'credit_date',
'credit_number',
'credit_issued_to',
'credit_to',
'your_credit',
'work_phone',
];
foreach ($fields as $field) {

View File

@ -33,7 +33,7 @@ trait SendsEmails
{
if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) {
$field = "email_subject_{$entityType}";
$value = $this->$field;
$value = $this->account_email_settings->$field;
if ($value) {
return preg_replace("/\r\n|\r|\n/", ' ', $value);
@ -84,7 +84,7 @@ trait SendsEmails
if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) {
$field = "email_template_{$entityType}";
$template = $this->$field;
$template = $this->account_email_settings->$field;
}
if (! $template) {
@ -158,20 +158,27 @@ trait SendsEmails
public function setTemplateDefaults($type, $subject, $body)
{
$settings = $this->account_email_settings;
if ($subject) {
$this->{"email_subject_" . $type} = $subject;
$settings->{"email_subject_" . $type} = $subject;
}
if ($body) {
$this->{"email_template_" . $type} = $body;
$settings->{"email_template_" . $type} = $body;
}
$this->save();
$settings->save();
}
public function getBccEmail()
{
return $this->isPro() ? $this->bcc_email : false;
return $this->isPro() ? $this->account_email_settings->bcc_email : false;
}
public function getReplyToEmail()
{
return $this->isPro() ? $this->account_email_settings->reply_to_email : false;
}
public function getFromEmail()

View File

@ -263,7 +263,7 @@ class User extends Authenticatable
// if the user changes their email then they need to reconfirm it
if ($user->isEmailBeingChanged()) {
$user->confirmed = 0;
$user->confirmation_code = str_random(RANDOM_KEY_LENGTH);
$user->confirmation_code = strtolower(str_random(RANDOM_KEY_LENGTH));
}
}

View File

@ -12,6 +12,7 @@ use Utils;
class AccountGatewayDatatable extends EntityDatatable
{
private static $accountGateways;
private static $accountGatewaySettings;
public $entityType = ENTITY_ACCOUNT_GATEWAY;
@ -19,7 +20,7 @@ class AccountGatewayDatatable extends EntityDatatable
{
return [
[
'name',
'gateway',
function ($model) {
if ($model->deleted_at) {
return $model->name;
@ -62,26 +63,16 @@ class AccountGatewayDatatable extends EntityDatatable
[
'limit',
function ($model) {
if ($model->gateway_id == GATEWAY_CUSTOM) {
$gatewayTypes = [GATEWAY_TYPE_CUSTOM];
} else {
$accountGateway = $this->getAccountGateway($model->id);
$paymentDriver = $accountGateway->paymentDriver();
$gatewayTypes = $paymentDriver->gatewayTypes();
$gatewayTypes = array_diff($gatewayTypes, [GATEWAY_TYPE_TOKEN]);
}
$gatewayTypes = $this->getGatewayTypes($model->id, $model->gateway_id);
$html = '';
foreach ($gatewayTypes as $gatewayTypeId) {
$accountGatewaySettings = AccountGatewaySettings::scope()->where('account_gateway_settings.gateway_type_id',
'=', $gatewayTypeId)->first();
$gatewayType = GatewayType::find($gatewayTypeId);
$accountGatewaySettings = $this->getAccountGatewaySetting($gatewayTypeId);
$gatewayType = Utils::getFromCache($gatewayTypeId, 'gatewayTypes');
if (count($gatewayTypes) > 1) {
if ($html) {
$html .= '<br>';
}
$html .= $gatewayType->name . ' &mdash; ';
}
@ -103,6 +94,38 @@ class AccountGatewayDatatable extends EntityDatatable
return $html;
},
],
[
'fees',
function ($model) {
if (! $model->gateway_fee_enabled) {
return trans('texts.fees_disabled');
}
$gatewayTypes = $this->getGatewayTypes($model->id, $model->gateway_id);
$html = '';
foreach ($gatewayTypes as $gatewayTypeId) {
$accountGatewaySettings = $this->getAccountGatewaySetting($gatewayTypeId);
if (! $accountGatewaySettings || ! $accountGatewaySettings->areFeesEnabled()) {
continue;
}
$gatewayType = Utils::getFromCache($gatewayTypeId, 'gatewayTypes');
if (count($gatewayTypes) > 1) {
if ($html) {
$html .= '<br>';
}
$html .= $gatewayType->name . ' &mdash; ';
}
$html .= $accountGatewaySettings->feesToString();
if ($accountGatewaySettings->hasTaxes()) {
$html .= ' + ' . trans('texts.tax');
}
};
return $html ?: trans('texts.no_fees');
},
],
];
}
@ -160,15 +183,9 @@ class AccountGatewayDatatable extends EntityDatatable
foreach (Cache::get('gatewayTypes') as $gatewayType) {
$actions[] = [
trans('texts.set_limits', ['gateway_type' => $gatewayType->name]),
trans('texts.set_limits_fees', ['gateway_type' => $gatewayType->name]),
function () use ($gatewayType) {
$accountGatewaySettings = AccountGatewaySettings::scope()
->where('account_gateway_settings.gateway_type_id', '=', $gatewayType->id)
->first();
$min = $accountGatewaySettings && $accountGatewaySettings->min_limit !== null ? $accountGatewaySettings->min_limit : 'null';
$max = $accountGatewaySettings && $accountGatewaySettings->max_limit !== null ? $accountGatewaySettings->max_limit : 'null';
return "javascript:showLimitsModal('{$gatewayType->name}', {$gatewayType->id}, $min, $max)";
return "javascript:showLimitsModal('{$gatewayType->name}', {$gatewayType->id})";
},
function ($model) use ($gatewayType) {
// Only show this action if the given gateway supports this gateway type
@ -176,10 +193,7 @@ class AccountGatewayDatatable extends EntityDatatable
return $gatewayType->id == GATEWAY_TYPE_CUSTOM;
} else {
$accountGateway = $this->getAccountGateway($model->id);
$paymentDriver = $accountGateway->paymentDriver();
$gatewayTypes = $paymentDriver->gatewayTypes();
return in_array($gatewayType->id, $gatewayTypes);
return $accountGateway->paymentDriver()->supportsGatewayType($gatewayType->id);
}
},
];
@ -198,4 +212,30 @@ class AccountGatewayDatatable extends EntityDatatable
return static::$accountGateways[$id];
}
private function getAccountGatewaySetting($gatewayTypeId)
{
if (isset(static::$accountGatewaySettings[$gatewayTypeId])) {
return static::$accountGatewaySettings[$gatewayTypeId];
}
static::$accountGatewaySettings[$gatewayTypeId] = AccountGatewaySettings::scope()
->where('account_gateway_settings.gateway_type_id', '=', $gatewayTypeId)->first();
return static::$accountGatewaySettings[$gatewayTypeId];
}
private function getGatewayTypes($id, $gatewayId)
{
if ($gatewayId == GATEWAY_CUSTOM) {
$gatewayTypes = [GATEWAY_TYPE_CUSTOM];
} else {
$accountGateway = $this->getAccountGateway($id);
$paymentDriver = $accountGateway->paymentDriver();
$gatewayTypes = $paymentDriver->gatewayTypes();
$gatewayTypes = array_diff($gatewayTypes, [GATEWAY_TYPE_TOKEN]);
}
return $gatewayTypes;
}
}

View File

@ -32,6 +32,13 @@ class ClientDatatable extends EntityDatatable
return link_to("clients/{$model->public_id}", $model->email ?: '')->toHtml();
},
],
[
'id_number',
function ($model) {
return $model->id_number;
},
Auth::user()->account->clientNumbersEnabled()
],
[
'client_created_at',
function ($model) {

View File

@ -41,10 +41,16 @@ class CreditDatatable extends EntityDatatable
'credit_date',
function ($model) {
if (! Auth::user()->can('viewByOwner', [ENTITY_CREDIT, $model->user_id])) {
return Utils::fromSqlDate($model->credit_date);
return Utils::fromSqlDate($model->credit_date_sql);
}
return link_to("credits/{$model->public_id}/edit", Utils::fromSqlDate($model->credit_date))->toHtml();
return link_to("credits/{$model->public_id}/edit", Utils::fromSqlDate($model->credit_date_sql))->toHtml();
},
],
[
'public_notes',
function ($model) {
return $model->public_notes;
},
],
[

View File

@ -49,10 +49,10 @@ class ExpenseDatatable extends EntityDatatable
'expense_date',
function ($model) {
if (! Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])) {
return Utils::fromSqlDate($model->expense_date);
return Utils::fromSqlDate($model->expense_date_sql);
}
return link_to("expenses/{$model->public_id}/edit", Utils::fromSqlDate($model->expense_date))->toHtml();
return link_to("expenses/{$model->public_id}/edit", Utils::fromSqlDate($model->expense_date_sql))->toHtml();
},
],
[
@ -73,7 +73,12 @@ class ExpenseDatatable extends EntityDatatable
[
'category',
function ($model) {
return $model->category != null ? substr($model->category, 0, 100) : '';
$category = $model->category != null ? substr($model->category, 0, 100) : '';
if (! Auth::user()->can('editByOwner', [ENTITY_EXPENSE_CATEGORY, $model->category_user_id])) {
return $category;
}
return $model->category_public_id ? link_to("expense_categories/{$model->category_public_id}/edit", $category)->toHtml() : '';
},
],
[

View File

@ -41,7 +41,7 @@ class InvoiceDatatable extends EntityDatatable
[
'date',
function ($model) {
return Utils::fromSqlDate($model->date);
return Utils::fromSqlDate($model->invoice_date);
},
],
[
@ -65,7 +65,7 @@ class InvoiceDatatable extends EntityDatatable
[
$entityType == ENTITY_INVOICE ? 'due_date' : 'valid_until',
function ($model) {
return Utils::fromSqlDate($model->due_date);
return Utils::fromSqlDate($model->due_date_sql);
},
],
[
@ -129,7 +129,7 @@ class InvoiceDatatable extends EntityDatatable
return "javascript:submitForm_{$entityType}('markPaid', {$model->public_id})";
},
function ($model) use ($entityType) {
return $entityType == ENTITY_INVOICE && $model->balance > 0 && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
return $entityType == ENTITY_INVOICE && $model->balance != 0 && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
},
],
[
@ -173,7 +173,7 @@ class InvoiceDatatable extends EntityDatatable
private function getStatusLabel($model)
{
$class = Invoice::calcStatusClass($model->invoice_status_id, $model->balance, $model->due_date, $model->is_recurring);
$class = Invoice::calcStatusClass($model->invoice_status_id, $model->balance, $model->due_date_sql, $model->is_recurring);
$label = Invoice::calcStatusLabel($model->invoice_status_name, $class, $this->entityType, $model->quote_invoice_id);
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";

View File

@ -92,7 +92,7 @@ class PaymentDatatable extends EntityDatatable
},
],
[
'payment_date',
'date',
function ($model) {
if ($model->is_deleted) {
return Utils::dateToString($model->payment_date);

View File

@ -5,6 +5,7 @@ namespace App\Ninja\Datatables;
use Auth;
use URL;
use Utils;
use App\Models\Invoice;
class RecurringInvoiceDatatable extends EntityDatatable
{
@ -32,19 +33,19 @@ class RecurringInvoiceDatatable extends EntityDatatable
[
'start_date',
function ($model) {
return Utils::fromSqlDate($model->start_date);
return Utils::fromSqlDate($model->start_date_sql);
},
],
[
'last_sent',
function ($model) {
return Utils::fromSqlDate($model->last_sent_date);
return Utils::fromSqlDate($model->last_sent_date_sql);
},
],
[
'end_date',
function ($model) {
return Utils::fromSqlDate($model->end_date);
return Utils::fromSqlDate($model->end_date_sql);
},
],
[
@ -53,9 +54,27 @@ class RecurringInvoiceDatatable extends EntityDatatable
return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id);
},
],
[
'status',
function ($model) {
return self::getStatusLabel($model);
},
],
];
}
private function getStatusLabel($model)
{
$class = Invoice::calcStatusClass($model->invoice_status_id, $model->balance, $model->due_date_sql, $model->is_recurring);
$label = Invoice::calcStatusLabel($model->invoice_status_name, $class, $this->entityType, $model->quote_invoice_id);
if ($model->invoice_status_id == INVOICE_STATUS_SENT && (! $model->last_sent_date_sql || $model->last_sent_date_sql == '0000-00-00')) {
$label = trans('texts.pending');
}
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
}
public function actions()
{
return [

View File

@ -39,7 +39,7 @@ class VendorDatatable extends EntityDatatable
},
],
[
'date',
'client_created_at',
function ($model) {
return Utils::timestampToDateString(strtotime($model->created_at));
},

View File

@ -36,7 +36,7 @@ class ContactMailer extends Mailer
*
* @return bool|null|string
*/
public function sendInvoice(Invoice $invoice, $reminder = false, $pdfString = false, $template = false)
public function sendInvoice(Invoice $invoice, $reminder = false, $template = false)
{
if ($invoice->is_recurring) {
return false;
@ -61,8 +61,9 @@ class ContactMailer extends Mailer
$emailSubject = !empty($template['subject']) ? $template['subject'] : $account->getEmailSubject($reminder ?: $entityType);
$sent = false;
$pdfString = false;
if ($account->attachPDF() && ! $pdfString) {
if ($account->attachPDF()) {
$pdfString = $invoice->getPDFString();
}
@ -198,7 +199,7 @@ class ContactMailer extends Mailer
}
$subject = $this->templateService->processVariables($subject, $variables);
$fromEmail = $user->email;
$fromEmail = $account->getReplyToEmail() ?: $user->email;
$view = $account->getTemplateView(ENTITY_INVOICE);
$response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, $view, $data);
@ -290,9 +291,10 @@ class ContactMailer extends Mailer
$data['invoice_id'] = $payment->invoice->id;
$view = $account->getTemplateView('payment_confirmation');
$fromEmail = $account->getReplyToEmail() ?: $user->email;
if ($user->email && $contact->email) {
$this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data);
$this->sendTo($contact->email, $fromEmail, $accountName, $subject, $view, $data);
}
$account->loadLocalizationSettings();

View File

@ -58,12 +58,7 @@ class UserMailer extends Mailer
$view = ($notificationType == 'approved' ? ENTITY_QUOTE : ENTITY_INVOICE) . "_{$notificationType}";
$account = $user->account;
$client = $invoice->client;
if ($account->hasMultipleAccounts()) {
$link = url(sprintf('/account/%s?redirect_to=%s', $account->account_key, $invoice->present()->path));
} else {
$link = $invoice->present()->url;
}
$link = $invoice->present()->multiAccountLink;
$data = [
'entityType' => $entityType,
@ -116,6 +111,26 @@ class UserMailer extends Mailer
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
/**
* @param Invitation $invitation
*/
public function sendMessage($user, $subject, $message, $invoice = false)
{
if (! $user->email) {
return;
}
$view = 'user_message';
$data = [
'userName' => $user->getDisplayName(),
'primaryMessage' => $subject,
'secondaryMessage' => $message,
'invoiceLink' => $invoice ? $invoice->present()->multiAccountLink : false,
];
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
}
public function sendSecurityCode($user, $code)
{
if (! $user->email) {

44
app/Ninja/OAuth/OAuth.php Normal file
View File

@ -0,0 +1,44 @@
<?php namespace App\Ninja\OAuth;
use App\Models\User;
class OAuth {
private $providerInstance;
public function __construct()
{
}
public function getProvider($provider)
{
switch ($provider)
{
case 'google';
$this->providerInstance = new Providers\Google();
return $this;
default:
return null;
break;
}
}
public function getTokenResponse($token)
{
$email = null;
$user = null;
if($this->providerInstance)
$user = User::where('email', $this->providerInstance->getTokenResponse($token))->first();
if ($user)
return $user;
else
return false;
}
}
?>

View File

@ -0,0 +1,23 @@
<?php namespace App\Ninja\OAuth\Providers;
class Google implements ProviderInterface
{
public function getTokenResponse($token)
{
$client = new \Google_Client(['client_id' => env('GOOGLE_CLIENT_ID','')]);
$payload = $client->verifyIdToken($token);
if ($payload)
return $this->harvestEmail($payload);
else
return null;
}
public function harvestEmail($payload)
{
return $payload['email'];
}
}

View File

@ -0,0 +1,9 @@
<?php namespace App\Ninja\OAuth\Providers;
interface ProviderInterface
{
public function getTokenResponse($token);
public function harvestEmail($response);
}

View File

@ -132,6 +132,12 @@ class BasePaymentDriver
return redirect()->to('view/' . $this->invitation->invitation_key);
}
if (! $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
// apply gateway fees
$invoicRepo = app('App\Ninja\Repositories\InvoiceRepository');
$invoicRepo->setGatewayFee($this->invoice(), $this->gatewayType);
}
if ($this->isGatewayType(GATEWAY_TYPE_TOKEN) || $gateway->is_offsite) {
if (Session::has('error')) {
Session::reflash();
@ -161,6 +167,7 @@ class BasePaymentDriver
'invoiceNumber' => $this->invoice()->invoice_number,
'client' => $this->client(),
'contact' => $this->invitation->contact,
'invitation' => $this->invitation,
'gatewayType' => $this->gatewayType,
'currencyId' => $this->client()->getCurrencyId(),
'currencyCode' => $this->client()->getCurrencyCode(),
@ -262,6 +269,9 @@ class BasePaymentDriver
->firstOrFail();
}
$invoicRepo = app('App\Ninja\Repositories\InvoiceRepository');
$invoicRepo->setGatewayFee($this->invoice(), $paymentMethod->payment_type->gateway_type_id);
if (! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) {
// The customer must have hacked the URL
Session::flash('error', trans('texts.limits_not_met'));
@ -854,6 +864,8 @@ class BasePaymentDriver
$label = trans('texts.payment_type_on_file', ['type' => $paymentMethod->payment_type->name]);
}
$label .= $this->invoice()->present()->gatewayFee($paymentMethod->payment_type->gateway_type_id);
$links[] = [
'url' => $url,
'label' => $label,
@ -886,6 +898,8 @@ class BasePaymentDriver
$label = trans("texts.{$gatewayTypeAlias}");
}
$label .= $this->invoice()->present()->gatewayFee($gatewayTypeId);
$links[] = [
'gatewayTypeId' => $gatewayTypeId,
'url' => $url,
@ -896,6 +910,11 @@ class BasePaymentDriver
return $links;
}
public function supportsGatewayType($gatewayTypeId)
{
return in_array($gatewayTypeId, $this->gatewayTypes());
}
protected function meetsGatewayTypeLimits($gatewayTypeId)
{
if (! $gatewayTypeId) {
@ -925,17 +944,6 @@ class BasePaymentDriver
$account = $this->account();
$url = URL::to("/payment/{$this->invitation->invitation_key}/{$gatewayTypeAlias}");
$gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias);
// PayPal doesn't allow being run in an iframe so we need to open in new tab
if ($gatewayTypeId === GATEWAY_TYPE_PAYPAL) {
$url .= '#braintree_paypal';
if ($account->iframe_url) {
return 'javascript:window.open("' . $url . '", "_blank")';
}
}
return $url;
}

View File

@ -5,6 +5,7 @@ namespace App\Ninja\PaymentDrivers;
use Braintree\Customer;
use Exception;
use Session;
use App\Models\GatewayType;
class BraintreePaymentDriver extends BasePaymentDriver
{
@ -62,6 +63,17 @@ class BraintreePaymentDriver extends BasePaymentDriver
return $customer instanceof Customer;
}
protected function paymentUrl($gatewayTypeAlias)
{
$url = parent::paymentUrl($gatewayTypeAlias);
if (GatewayType::getIdFromAlias($gatewayTypeAlias) === GATEWAY_TYPE_PAYPAL) {
$url .= '#braintree_paypal';
}
return $url;
}
protected function paymentDetails($paymentMethod = false)
{
$data = parent::paymentDetails($paymentMethod);

View File

@ -17,6 +17,7 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
$data['ButtonSource'] = 'InvoiceNinja_SP';
$data['solutionType'] = 'Sole'; // show 'Pay with credit card' option
$data['transactionId'] = $data['transactionId'] . '-' . time();
return $data;
}
@ -27,4 +28,17 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
return $payment;
}
protected function paymentUrl($gatewayTypeAlias)
{
$url = parent::paymentUrl($gatewayTypeAlias);
// PayPal doesn't allow being run in an iframe so we need to open in new tab
if ($this->account()->iframe_url) {
return 'javascript:window.open("' . $url . '", "_blank")';
} else {
return $url;
}
}
}

View File

@ -56,12 +56,14 @@ class WePayPaymentDriver extends BasePaymentDriver
$data['transaction_id'] = $transactionId;
}
$data['applicationFee'] = (env('WEPAY_APP_FEE_MULTIPLIER') * $data['amount']) + env('WEPAY_APP_FEE_FIXED');
$data['feePayer'] = env('WEPAY_FEE_PAYER');
$data['callbackUri'] = $this->accountGateway->getWebhookUrl();
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod)) {
$data['paymentMethodType'] = 'payment_bank';
$data['applicationFee'] = (env('WEPAY_APP_FEE_ACH_MULTIPLIER') * $data['amount']) + env('WEPAY_APP_FEE_FIXED');
} else {
$data['applicationFee'] = (env('WEPAY_APP_FEE_CC_MULTIPLIER') * $data['amount']) + env('WEPAY_APP_FEE_FIXED');
}
$data['transaction_rbits'] = $this->invoice()->present()->rBits;

View File

@ -149,4 +149,28 @@ class AccountPresenter extends Presenter
return $options;
}
public function customTextFields()
{
$fields = [
'custom_client_label1' => 'custom_client1',
'custom_client_label2' => 'custom_client2',
'custom_invoice_text_label1' => 'custom_invoice1',
'custom_invoice_text_label2' => 'custom_invoice2',
'custom_invoice_item_label1' => 'custom_product1',
'custom_invoice_item_label2' => 'custom_product2',
];
$data = [];
foreach ($fields as $key => $val) {
if ($this->$key) {
$data[$this->$key] = [
'value' => $val,
'name' => $val,
];
}
}
return $data;
}
}

View File

@ -225,7 +225,7 @@ class InvoicePresenter extends EntityPresenter
$actions[] = ['url' => url("quotes/{$invoice->quote_id}/edit"), 'label' => trans('texts.view_quote')];
}
if (!$invoice->deleted_at && ! $invoice->is_recurring && $invoice->balance > 0) {
if (!$invoice->deleted_at && ! $invoice->is_recurring && $invoice->balance != 0) {
$actions[] = ['url' => 'javascript:submitBulkAction("markPaid")', 'label' => trans('texts.mark_paid')];
$actions[] = ['url' => 'javascript:onPaymentClick()', 'label' => trans('texts.enter_payment')];
}
@ -252,4 +252,45 @@ class InvoicePresenter extends EntityPresenter
return $actions;
}
public function gatewayFee($gatewayTypeId = false)
{
$invoice = $this->entity;
$account = $invoice->account;
if (! $account->gateway_fee_enabled) {
return '';
}
$settings = $account->getGatewaySettings($gatewayTypeId);
if (! $settings || ! $settings->areFeesEnabled()) {
return '';
}
$fee = $invoice->calcGatewayFee($gatewayTypeId, true);
$fee = $account->formatMoney($fee, $invoice->client);
if (floatval($settings->fee_amount) < 0 || floatval($settings->fee_percent) < 0) {
$label = trans('texts.discount');
} else {
$label = trans('texts.fee');
}
return ' - ' . $fee . ' ' . $label;
}
public function multiAccountLink()
{
$invoice = $this->entity;
$account = $invoice->account;
if ($account->hasMultipleAccounts()) {
$link = url(sprintf('/account/%s?redirect_to=%s', $account->account_key, $invoice->present()->path));
} else {
$link = $invoice->present()->url;
}
return $link;
}
}

View File

@ -25,6 +25,7 @@ class AbstractReport
public function run()
{
}
public function results()
@ -66,7 +67,7 @@ class AbstractReport
if (strpos($field, 'date') !== false) {
$class[] = 'group-date-' . (isset($this->options['group_dates_by']) ? $this->options['group_dates_by'] : 'monthyear');
} elseif (in_array($field, ['client', 'vendor', 'product', 'method', 'category'])) {
} elseif (in_array($field, ['client', 'vendor', 'product', 'user', 'method', 'category', 'project'])) {
$class[] = 'group-letter-100';
} elseif (in_array($field, ['amount', 'paid', 'balance'])) {
$class[] = 'group-number-50';

View File

@ -0,0 +1,41 @@
<?php
namespace App\Ninja\Reports;
use App\Models\Activity;
use Auth;
class ActivityReport extends AbstractReport
{
public $columns = [
'date',
'client',
'user',
'activity',
];
public function run()
{
$account = Auth::user()->account;
$startDate = $this->startDate->format('Y-m-d');
$endDate = $this->endDate->format('Y-m-d');
$activities = Activity::scope()
->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'task', 'expense', 'account')
->whereRaw("DATE(created_at) >= \"{$startDate}\" and DATE(created_at) <= \"$endDate\"")
->orderBy('id', 'desc');
foreach ($activities->get() as $activity) {
$client = $activity->client;
$this->data[] = [
$activity->present()->createdAt,
$client ? ($this->isExport ? $client->getDisplayName() : $client->present()->link) : '',
$activity->present()->user,
$activity->getMessage(),
];
}
}
}

View File

@ -22,6 +22,7 @@ class AgingReport extends AbstractReport
$account = Auth::user()->account;
$clients = Client::scope()
->orderBy('name')
->withArchived()
->with('contacts')
->with(['invoices' => function ($query) {

Some files were not shown because too many files have changed in this diff Show More