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_URL=http://ninja.dev
APP_KEY=SomeRandomStringSomeRandomString APP_KEY=SomeRandomStringSomeRandomString
APP_CIPHER=AES-256-CBC APP_CIPHER=AES-256-CBC
APP_LOCALE=en
DB_TYPE=mysql DB_TYPE=mysql
DB_STRICT=false DB_STRICT=false
@ -90,7 +91,8 @@ WEPAY_ENVIRONMENT=production # production or stage
WEPAY_AUTO_UPDATE=true # Requires permission from WePay WEPAY_AUTO_UPDATE=true # Requires permission from WePay
WEPAY_ENABLE_CANADA=true WEPAY_ENABLE_CANADA=true
WEPAY_FEE_PAYER=payee 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_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 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_ENV=production/APP_ENV=development/g' .env
- sed -i 's/APP_DEBUG=false/APP_DEBUG=true/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/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 NINJA_DEV=true' .env
- sed -i '$a TRAVIS=true' .env - sed -i '$a TRAVIS=true' .env
# create the database and user # create the database and user
@ -58,7 +59,6 @@ before_script:
# migrate and seed the database # migrate and seed the database
- php artisan migrate --no-interaction - php artisan migrate --no-interaction
- php artisan db:seed --no-interaction # default seed - php artisan db:seed --no-interaction # default seed
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed
# Start webserver on ninja.dev:8000 # Start webserver on ninja.dev:8000
- php artisan serve --host=ninja.dev --port=8000 & # '&' allows to run in background - php artisan serve --host=ninja.dev --port=8000 & # '&' allows to run in background
# Start PhantomJS # Start PhantomJS
@ -67,10 +67,10 @@ before_script:
- sleep 5 - sleep 5
# Make sure the app is up-to-date # Make sure the app is up-to-date
- curl -L http://ninja.dev:8000/update - 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: 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 APICest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance CheckBalanceCest.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 OnlinePaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance PaymentCest.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 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 #- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env
#- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php #- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php
after_script: after_script:
- php artisan ninja:check-data --no-interaction
- cat .env - cat .env
- mysql -u root -e 'select * from accounts;' ninja - 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 account_gateways;' ninja
- mysql -u root -e 'select * from clients;' 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 invoices;' ninja
- mysql -u root -e 'select * from invoice_items;' 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 payments;' ninja
- mysql -u root -e 'select * from credits;' ninja - mysql -u root -e 'select * from credits;' ninja
- mysql -u root -e 'select * from expenses;' ninja - mysql -u root -e 'select * from expenses;' ninja
- cat storage/logs/laravel-error.log - cat storage/logs/laravel-error.log
- cat storage/logs/laravel-info.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 - for i in $FILES; do echo $i; base64 "$i"; break; done
notifications: notifications:

View File

@ -4,6 +4,8 @@ Thanks for your contributions!
## Submit bug reports or feature requests ## Submit bug reports or feature requests
Please discuss the changes with us ahead of time to ensure they will be merged.
### Submit pull requests ### Submit pull requests
* [Fork](https://github.com/invoiceninja/invoiceninja#fork-destination-box) the [Invoice Ninja repository](https://github.com/invoiceninja/invoiceninja) * [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` * Create a new branch with the name `#issue_number-Short-description`
@ -11,7 +13,7 @@ Thanks for your contributions!
* Make your changes and commit * Make your changes and commit
* Check if your branch is still in sync with the repositorys **`develop`** branch * 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/) * _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 * Push your branch and create a PR against the Invoice Ninja **`develop`** branch
* Update the [Changelog](CHANGELOG.md) * 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. 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 issue. If you want to submit more, create multiple issues.
* Only one feature/bugfix per PR(pull request). Split more changes into multiple PRs. * Only one feature/bugfix per PR(pull request). Split more changes into multiple PRs.
#### Coding Style #### 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) 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 ```php
/** /**
* Gets a preview of the email * Gets a preview of the email
* *
* @param TemplateService $templateService * @param TemplateService $templateService
* *
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response

View File

@ -4,6 +4,7 @@ namespace App\Console\Commands;
use Carbon; use Carbon;
use DB; use DB;
use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Mail; use Mail;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -83,6 +84,8 @@ class CheckData extends Command
->from(CONTACT_EMAIL) ->from(CONTACT_EMAIL)
->subject('Check-Data: ' . strtoupper($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE)); ->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' => [ 'products' => [
ENTITY_USER, ENTITY_USER,
], ],
'vendors' => [
ENTITY_USER,
],
'expense_categories' => [ 'expense_categories' => [
ENTITY_USER, ENTITY_USER,
], ],
'payment_terms' => [
ENTITY_USER,
],
'projects' => [ 'projects' => [
ENTITY_USER, ENTITY_USER,
ENTITY_CLIENT, ENTITY_CLIENT,

View File

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

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Invoice; use App\Models\Invoice;
use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
@ -57,9 +58,18 @@ class SendRecurringInvoices extends Command
public function fire() 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(); $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') $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]) ->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') ->orderBy('id', 'asc')
@ -74,7 +84,8 @@ class SendRecurringInvoices extends Command
continue; continue;
} }
$recurInvoice->account->loadLocalizationSettings($recurInvoice->client); $account = $recurInvoice->account;
$account->loadLocalizationSettings($recurInvoice->client);
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice); $invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
if ($invoice && ! $invoice->isPaid()) { 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( * @SWG\Get(
* path="/$LOWER_NAME$", * path="/$LOWER_NAME$",
* summary="List of $LOWER_NAME$", * summary="List $LOWER_NAME$",
* operationId="list$STUDLY_NAME$s",
* tags={"$LOWER_NAME$"}, * tags={"$LOWER_NAME$"},
* @SWG\Response( * @SWG\Response(
* response=200, * 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\Schema(type="array", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -47,7 +48,14 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
* @SWG\Get( * @SWG\Get(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}", * path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* summary="Individual $STUDLY_NAME$", * summary="Individual $STUDLY_NAME$",
* operationId="get$STUDLY_NAME$",
* tags={"$LOWER_NAME$"}, * tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="path",
* name="$LOWER_NAME$_id",
* type="integer",
* required=true
* ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A single $LOWER_NAME$", * description="A single $LOWER_NAME$",
@ -59,7 +67,6 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
* ) * )
* ) * )
*/ */
public function show($STUDLY_NAME$Request $request) public function show($STUDLY_NAME$Request $request)
{ {
return $this->itemResponse($request->entity()); return $this->itemResponse($request->entity());
@ -71,11 +78,12 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
/** /**
* @SWG\Post( * @SWG\Post(
* path="/$LOWER_NAME$", * path="/$LOWER_NAME$",
* tags={"$LOWER_NAME$"},
* summary="Create a $LOWER_NAME$", * summary="Create a $LOWER_NAME$",
* operationId="create$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="$LOWER_NAME$",
* @SWG\Schema(ref="#/definitions/$STUDLY_NAME$") * @SWG\Schema(ref="#/definitions/$STUDLY_NAME$")
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -99,16 +107,23 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
/** /**
* @SWG\Put( * @SWG\Put(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}", * path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* tags={"$LOWER_NAME$"},
* summary="Update a $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( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="$LOWER_NAME$",
* @SWG\Schema(ref="#/definitions/$STUDLY_NAME$") * @SWG\Schema(ref="#/definitions/$STUDLY_NAME$")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Update $LOWER_NAME$", * description="Updated $LOWER_NAME$",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -117,7 +132,6 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
* ) * )
* ) * )
*/ */
public function update(Update$STUDLY_NAME$Request $request, $publicId) public function update(Update$STUDLY_NAME$Request $request, $publicId)
{ {
if ($request->action) { if ($request->action) {
@ -133,16 +147,18 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
/** /**
* @SWG\Delete( * @SWG\Delete(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}", * path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* tags={"$LOWER_NAME$"},
* summary="Delete a $LOWER_NAME$", * summary="Delete a $LOWER_NAME$",
* operationId="delete$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="path",
* name="body", * name="$LOWER_NAME$_id",
* @SWG\Schema(ref="#/definitions/$STUDLY_NAME$") * type="integer",
* required=true
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Delete $LOWER_NAME$", * description="Deleted $LOWER_NAME$",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -151,7 +167,6 @@ class $STUDLY_NAME$ApiController extends BaseAPIController
* ) * )
* ) * )
*/ */
public function destroy(Update$STUDLY_NAME$Request $request) public function destroy(Update$STUDLY_NAME$Request $request)
{ {
$$LOWER_NAME$ = $request->entity(); $$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="id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="user_id", type="integer", example=1) * @SWG\Property(property="user_id", type="integer", example=1)
* @SWG\Property(property="account_key", type="string", example="123456") * @SWG\Property(property="account_key", type="string", example="123456")
* @SWG\Property(property="updated_at", type="timestamp", example="") * @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="archived_at", type="timestamp", example="1451160233") * @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_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2); 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_CONTACT', 'contact');
define('PERSON_USER', 'user'); define('PERSON_USER', 'user');
define('PERSON_VENDOR_CONTACT', 'vendorcontact'); define('PERSON_VENDOR_CONTACT', 'vendorcontact');
@ -283,7 +288,6 @@ if (! defined('APP_NAME')) {
define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN'); define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN');
define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID'); define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID');
define('PREV_USER_ID', 'PREV_USER_ID');
define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h');
define('NINJA_LICENSE_ACCOUNT_KEY', 'AsFmBAeLXF0IKf7tmi0eiyZfmWW9hxMT'); define('NINJA_LICENSE_ACCOUNT_KEY', 'AsFmBAeLXF0IKf7tmi0eiyZfmWW9hxMT');
define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); 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_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_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01'); 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_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.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\Foundation\Validation\ValidationException;
use Illuminate\Http\Exception\HttpResponseException; use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Session\TokenMismatchException;
use Redirect; use Redirect;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -26,10 +27,11 @@ class Handler extends ExceptionHandler
* @var array * @var array
*/ */
protected $dontReport = [ protected $dontReport = [
AuthorizationException::class, TokenMismatchException::class,
HttpException::class,
ModelNotFoundException::class, ModelNotFoundException::class,
ValidationException::class, //AuthorizationException::class,
//HttpException::class,
//ValidationException::class,
]; ];
/** /**
@ -43,11 +45,20 @@ class Handler extends ExceptionHandler
*/ */
public function report(Exception $e) public function report(Exception $e)
{ {
if (! $this->shouldReport($e)) {
return false;
}
// don't show these errors in the logs // don't show these errors in the logs
if ($e instanceof NotFoundHttpException) { if ($e instanceof NotFoundHttpException) {
if (Crawler::isCrawler()) { if (Crawler::isCrawler()) {
return false; 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) { } elseif ($e instanceof HttpResponseException) {
return false; return false;
} }
@ -74,9 +85,9 @@ class Handler extends ExceptionHandler
if ($e instanceof ModelNotFoundException) { if ($e instanceof ModelNotFoundException) {
return Redirect::to('/'); return Redirect::to('/');
} }
if ($e instanceof \Illuminate\Session\TokenMismatchException) {
// prevent loop since the page auto-submits if ($e instanceof TokenMismatchException) {
if ($request->path() != 'get_started') { if (! in_array($request->path(), ['get_started', 'save_sidebar_state'])) {
// https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e // https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e
return redirect() return redirect()
->back() ->back()

View File

@ -6,6 +6,7 @@ use App\Events\UserSignedUp;
use App\Http\Requests\RegisterRequest; use App\Http\Requests\RegisterRequest;
use App\Http\Requests\UpdateAccountRequest; use App\Http\Requests\UpdateAccountRequest;
use App\Models\Account; use App\Models\Account;
use App\Ninja\OAuth\OAuth;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Transformers\AccountTransformer; use App\Ninja\Transformers\AccountTransformer;
use App\Ninja\Transformers\UserAccountTransformer; use App\Ninja\Transformers\UserAccountTransformer;
@ -121,6 +122,7 @@ class AccountApiController extends BaseAPIController
for ($x = 0; $x < count($devices); $x++) { for ($x = 0; $x < count($devices); $x++) {
if ($devices[$x]['email'] == Auth::user()->username) { if ($devices[$x]['email'] == Auth::user()->username) {
$devices[$x]['token'] = $request->token; //update $devices[$x]['token'] = $request->token; //update
$devices[$x]['device'] = $request->device;
$account->devices = json_encode($devices); $account->devices = json_encode($devices);
$account->save(); $account->save();
$devices[$x]['account_key'] = $account->account_key; $devices[$x]['account_key'] = $account->account_key;
@ -187,25 +189,15 @@ class AccountApiController extends BaseAPIController
$token = $request->input('token'); $token = $request->input('token');
$provider = $request->input('provider'); $provider = $request->input('provider');
try { $oAuth = new OAuth();
$user = Socialite::driver($provider)->stateless()->userFromToken($token); $user = $oAuth->getProvider($provider)->getTokenResponse($token);
} catch (Exception $exception) {
return $this->errorResponse(['message' => $exception->getMessage()], 401);
}
if ($user) { if($user) {
$providerId = AuthService::getProviderId($provider);
$user = $this->accountRepo->findUserByOauth($providerId, $user->id);
}
if ($user) {
Auth::login($user); Auth::login($user);
return $this->processLogin($request); 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\Http\Requests\UpdateAccountRequest;
use App\Models\Account; use App\Models\Account;
use App\Models\AccountGateway; use App\Models\AccountGateway;
use App\Models\AccountGatewaySettings;
use App\Models\Affiliate; use App\Models\Affiliate;
use App\Models\Document; use App\Models\Document;
use App\Models\Gateway; use App\Models\Gateway;
@ -38,6 +37,7 @@ use Request;
use Response; use Response;
use Session; use Session;
use stdClass; use stdClass;
use Exception;
use URL; use URL;
use Utils; use Utils;
@ -123,17 +123,16 @@ class AccountController extends BaseController
{ {
$user = false; $user = false;
$guestKey = Input::get('guest_key'); // local storage key to login until registered $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()) { if (Auth::check()) {
return Redirect::to('invoices/create'); return Redirect::to('invoices/create');
} }
if (! Utils::isNinja() && (Account::count() > 0 && ! $prevUserId)) { if (! Utils::isNinja() && Account::count() > 0) {
return Redirect::to('/login'); return Redirect::to('/login');
} }
if ($guestKey && ! $prevUserId) { if ($guestKey) {
$user = User::where('password', '=', $guestKey)->first(); $user = User::where('password', '=', $guestKey)->first();
if ($user && $user->registered) { if ($user && $user->registered) {
@ -144,11 +143,6 @@ class AccountController extends BaseController
if (! $user) { if (! $user) {
$account = $this->accountRepo->create(); $account = $this->accountRepo->create();
$user = $account->users()->first(); $user = $account->users()->first();
if ($prevUserId) {
$users = $this->accountRepo->associateAccounts($user->id, $prevUserId);
Session::put(SESSION_USER_ACCOUNTS, $users);
}
} }
Auth::login($user, true); Auth::login($user, true);
@ -186,22 +180,8 @@ class AccountController extends BaseController
$newPlan['price'] = Utils::getPlanPrice($newPlan); $newPlan['price'] = Utils::getPlanPrice($newPlan);
$credit = 0; $credit = 0;
if (! empty($planDetails['started']) && $plan == PLAN_FREE) { if ($plan == PLAN_FREE && $company->processRefund(Auth::user())) {
// Downgrade Session::flash('warning', trans('texts.plan_refunded'));
$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'));
}
}
} }
$hasPaid = false; $hasPaid = false;
@ -241,6 +221,8 @@ class AccountController extends BaseController
$company->plan = $plan; $company->plan = $plan;
$company->save(); $company->save();
Session::flash('message', trans('texts.updated_plan'));
return Redirect::to('settings/account_management'); return Redirect::to('settings/account_management');
} }
} }
@ -488,23 +470,19 @@ class AccountController extends BaseController
} }
} }
if ($trashedCount == 0) { $tokenBillingOptions = [];
return Redirect::to('gateways/create'); for ($i = 1; $i <= 4; $i++) {
} else { $tokenBillingOptions[$i] = trans("texts.token_billing_{$i}");
$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,
]);
} }
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 = $request->user()->account;
$account->fill($request->all()); $account->fill($request->all());
$account->bcc_email = $request->bcc_email;
$account->save(); $account->save();
$settings = $account->account_email_settings;
$settings->fill($request->all());
$settings->save();
return redirect('settings/' . ACCOUNT_EMAIL_SETTINGS) return redirect('settings/' . ACCOUNT_EMAIL_SETTINGS)
->with('message', trans('texts.updated_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) { foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) {
$subjectField = "email_subject_{$type}"; $subjectField = "email_subject_{$type}";
$subject = Input::get($subjectField, $account->getEmailSubject($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}"; $bodyField = "email_template_{$type}";
$body = Input::get($bodyField, $account->getEmailTemplate($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) { foreach ([REMINDER1, REMINDER2, REMINDER3] as $type) {
@ -846,6 +827,7 @@ class AccountController extends BaseController
} }
$account->save(); $account->save();
$account->account_email_settings->save();
Session::flash('message', trans('texts.updated_settings')); 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_prefix = trim(Input::get('client_number_prefix'));
$account->client_number_pattern = trim(Input::get('client_number_pattern')); $account->client_number_pattern = trim(Input::get('client_number_pattern'));
$account->client_number_counter = Input::get('client_number_counter'); $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')) { if (Input::has('recurring_hour')) {
$account->recurring_hour = Input::get('recurring_hour'); $account->recurring_hour = Input::get('recurring_hour');
@ -1054,28 +1038,32 @@ class AccountController extends BaseController
$size = filesize($filePath); $size = filesize($filePath);
if ($size / 1000 > MAX_DOCUMENT_SIZE) { if ($size / 1000 > MAX_DOCUMENT_SIZE) {
Session::flash('warning', 'File too large'); Session::flash('warning', trans('texts.logo_warning_too_large'));
} else { } else {
if ($documentType != 'gif') { if ($documentType != 'gif') {
$account->logo = $account->account_key.'.'.$documentType; $account->logo = $account->account_key.'.'.$documentType;
$imageSize = getimagesize($filePath); try {
$account->logo_width = $imageSize[0]; $imageSize = getimagesize($filePath);
$account->logo_height = $imageSize[1]; $account->logo_width = $imageSize[0];
$account->logo_size = $size; $account->logo_height = $imageSize[1];
$account->logo_size = $size;
// make sure image isn't interlaced // make sure image isn't interlaced
if (extension_loaded('fileinfo')) { if (extension_loaded('fileinfo')) {
$image = Image::make($path); $image = Image::make($path);
$image->interlace(false); $image->interlace(false);
$imageStr = (string) $image->encode($documentType); $imageStr = (string) $image->encode($documentType);
$disk->put($account->logo, $imageStr); $disk->put($account->logo, $imageStr);
$account->logo_size = strlen($imageStr); $account->logo_size = strlen($imageStr);
} else { } else {
$stream = fopen($filePath, 'r'); $stream = fopen($filePath, 'r');
$disk->getDriver()->putStream($account->logo, $stream, ['mimetype' => $documentTypeData['mime']]); $disk->getDriver()->putStream($account->logo, $stream, ['mimetype' => $documentTypeData['mime']]);
fclose($stream); fclose($stream);
}
} catch (Exception $exception) {
Session::flash('warning', trans('texts.logo_warning_invalid'));
} }
} else { } else {
if (extension_loaded('fileinfo')) { if (extension_loaded('fileinfo')) {
@ -1093,7 +1081,7 @@ class AccountController extends BaseController
$account->logo_width = $image->width(); $account->logo_width = $image->width();
$account->logo_height = $image->height(); $account->logo_height = $image->height();
} else { } 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(); $user->referral_code = $this->accountRepo->getReferralCode();
} }
} }
if (Utils::isNinjaDev()) {
$user->dark_mode = Input::get('dark_mode') ? true : false;
}
$user->save(); $user->save();
@ -1189,6 +1174,8 @@ class AccountController extends BaseController
$account = Auth::user()->account; $account = Auth::user()->account;
$account->token_billing_type_id = Input::get('token_billing_type_id'); $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->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(); $account->save();
event(new UserSettingsChanged()); event(new UserSettingsChanged());
@ -1198,35 +1185,6 @@ class AccountController extends BaseController
return Redirect::to('settings/'.ACCOUNT_PAYMENTS); 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 * @return \Illuminate\Http\RedirectResponse
*/ */
@ -1255,7 +1213,7 @@ class AccountController extends BaseController
public function checkEmail() public function checkEmail()
{ {
$email = User::withTrashed()->where('email', '=', Input::get('email')) $email = User::withTrashed()->where('email', '=', Input::get('email'))
->where('id', '<>', Auth::user()->id) ->where('id', '<>', Auth::user()->registered ? 0 : Auth::user()->id)
->first(); ->first();
if ($email) { if ($email) {
@ -1270,36 +1228,58 @@ class AccountController extends BaseController
*/ */
public function submitSignup() public function submitSignup()
{ {
$user = Auth::user();
$account = $user->account;
$rules = [ $rules = [
'new_first_name' => 'required', 'new_first_name' => 'required',
'new_last_name' => 'required', 'new_last_name' => 'required',
'new_password' => 'required|min:6', '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); $validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) { if ($validator->fails()) {
return ''; return '';
} }
/** @var \App\Models\User $user */ $firstName = trim(Input::get('new_first_name'));
$user = Auth::user(); $lastName = trim(Input::get('new_last_name'));
$user->first_name = trim(Input::get('new_first_name')); $email = trim(strtolower(Input::get('new_email')));
$user->last_name = trim(Input::get('new_last_name')); $password = trim(Input::get('new_password'));
$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();
$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::flash('message', trans('texts.created_new_company'));
Session::set(REQUESTED_PRO_PLAN, true); 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 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 * @return \Illuminate\Http\RedirectResponse
*/ */
@ -1350,6 +1340,9 @@ class AccountController extends BaseController
$account = Auth::user()->account; $account = Auth::user()->account;
\Log::info("Canceled Account: {$account->name} - {$user->email}"); \Log::info("Canceled Account: {$account->name} - {$user->email}");
$company = $account->company;
$refunded = $company->processRefund(Auth::user());
Document::scope()->each(function ($item, $key) { Document::scope()->each(function ($item, $key) {
$item->delete(); $item->delete();
}); });
@ -1365,6 +1358,10 @@ class AccountController extends BaseController
Auth::logout(); Auth::logout();
Session::flush(); Session::flush();
if ($refunded) {
Session::flash('warning', trans('texts.plan_refunded'));
}
return Redirect::to('/')->with('clearGuestKey', true); return Redirect::to('/')->with('clearGuestKey', true);
} }
@ -1414,18 +1411,17 @@ class AccountController extends BaseController
public function previewEmail(TemplateService $templateService) public function previewEmail(TemplateService $templateService)
{ {
$template = Input::get('template'); $template = Input::get('template');
$invoice = Invoice::scope() $invitation = \App\Models\Invitation::scope()
->invoices() ->with('invoice.client.contacts')
->withTrashed() ->first();
->first();
if (! $invoice) { if (! $invitation) {
return trans('texts.create_invoice_for_sample'); return trans('texts.create_invoice_for_sample');
} }
/** @var \App\Models\Account $account */ /** @var \App\Models\Account $account */
$account = Auth::user()->account; $account = Auth::user()->account;
$invitation = $invoice->invitations->first(); $invoice = $invitation->invoice;
// replace the variables with sample data // replace the variables with sample data
$data = [ $data = [

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Account; use App\Models\Account;
use App\Models\AccountGatewaySettings;
use App\Models\AccountGateway; use App\Models\AccountGateway;
use App\Models\Gateway; use App\Models\Gateway;
use App\Services\AccountGatewayService; use App\Services\AccountGatewayService;
@ -131,6 +132,10 @@ class AccountGatewayController extends BaseController
$currentGateways = $account->account_gateways; $currentGateways = $account->account_gateways;
$gateways = Gateway::where('payment_library_id', '=', 1)->orderBy('name')->get(); $gateways = Gateway::where('payment_library_id', '=', 1)->orderBy('name')->get();
if ($accountGateway) {
$accountGateway->fields = [];
}
foreach ($gateways as $gateway) { foreach ($gateways as $gateway) {
$fields = $gateway->getFields(); $fields = $gateway->getFields();
if (! $gateway->isCustom()) { if (! $gateway->isCustom()) {
@ -372,7 +377,7 @@ class AccountGatewayController extends BaseController
'tos_agree' => 'required', 'tos_agree' => 'required',
'first_name' => 'required', 'first_name' => 'required',
'last_name' => 'required', 'last_name' => 'required',
'email' => 'required', 'email' => 'required|email',
]; ];
if (WEPAY_ENABLE_CANADA) { if (WEPAY_ENABLE_CANADA) {
@ -387,6 +392,13 @@ class AccountGatewayController extends BaseController
->withInput(); ->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 { try {
$wepay = Utils::setupWePay(); $wepay = Utils::setupWePay();
@ -494,4 +506,33 @@ class AccountGatewayController extends BaseController
return Redirect::to("gateways/{$accountGateway->public_id}/edit"); 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'); $test = Input::get('test');
$app = Input::get('app'); $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['debug'] = Input::get('debug') ? 'true' : 'false';
$app['https'] = Input::get('https') ? '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_FROM_ADDRESS'] = $mail['from']['address'];
$_ENV['MAIL_PASSWORD'] = $mail['password']; $_ENV['MAIL_PASSWORD'] = $mail['password'];
$_ENV['PHANTOMJS_CLOUD_KEY'] = 'a-demo-key-with-low-quota-per-ip-address'; $_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_DOMAIN'] = $mail['mailgun_domain'];
$_ENV['MAILGUN_SECRET'] = $mail['mailgun_secret']; $_ENV['MAILGUN_SECRET'] = $mail['mailgun_secret'];
@ -191,7 +191,8 @@ class AppController extends BaseController
$config .= "{$key}={$val}\n"; $config .= "{$key}={$val}\n";
} }
$fp = fopen(base_path().'/.env', 'w'); $filePath = base_path().'/.env';
$fp = fopen($filePath, 'w');
fwrite($fp, $config); fwrite($fp, $config);
fclose($fp); fclose($fp);
@ -345,6 +346,16 @@ class AppController extends BaseController
return RESULT_SUCCESS; return RESULT_SUCCESS;
} }
public function checkData()
{
try {
Artisan::call('ninja:check-data');
return RESULT_SUCCESS;
} catch (Exception $exception) {
return RESULT_FAILURE;
}
}
public function stats() public function stats()
{ {
if (! hash_equals(Input::get('password'), env('RESELLER_PASSWORD'))) { if (! hash_equals(Input::get('password'), env('RESELLER_PASSWORD'))) {

View File

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

View File

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

View File

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

View File

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

View File

@ -68,6 +68,7 @@ class ClientPortalController extends BaseController
} }
$account->loadLocalizationSettings($client); $account->loadLocalizationSettings($client);
$this->invoiceRepo->clearGatewayFee($invoice);
if (! Input::has('phantomjs') && ! session('silent:' . $client->id) && ! Session::has($invitation->invitation_key) if (! Input::has('phantomjs') && ! session('silent:' . $client->id) && ! Session::has($invitation->invitation_key)
&& (! Auth::check() || Auth::user()->account_id != $invoice->account_id)) { && (! Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
@ -146,6 +147,7 @@ class ClientPortalController extends BaseController
'paymentTypes' => $paymentTypes, 'paymentTypes' => $paymentTypes,
'paymentURL' => $paymentURL, 'paymentURL' => $paymentURL,
'phantomjs' => Input::has('phantomjs'), 'phantomjs' => Input::has('phantomjs'),
'gatewayTypeId' => count($paymentTypes) == 1 ? $paymentTypes[0]['gatewayTypeId'] : false,
]; ];
if ($paymentDriver = $account->paymentDriver($invitation, GATEWAY_TYPE_CREDIT_CARD)) { if ($paymentDriver = $account->paymentDriver($invitation, GATEWAY_TYPE_CREDIT_CARD)) {
@ -521,7 +523,7 @@ class ClientPortalController extends BaseController
'account' => $account, 'account' => $account,
'title' => trans('texts.credits'), 'title' => trans('texts.credits'),
'entityType' => ENTITY_CREDIT, '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); 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; namespace App\Http\Controllers;
use App\Http\Requests\CreateDocumentRequest;
use App\Http\Requests\DocumentRequest; use App\Http\Requests\DocumentRequest;
use App\Http\Requests\CreateDocumentRequest;
use App\Http\Requests\UpdateDocumentRequest;
use App\Models\Document; use App\Models\Document;
use App\Ninja\Repositories\DocumentRepository; use App\Ninja\Repositories\DocumentRepository;
@ -37,11 +38,12 @@ class DocumentAPIController extends BaseAPIController
/** /**
* @SWG\Get( * @SWG\Get(
* path="/documents", * path="/documents",
* summary="List of document", * summary="List document",
* operationId="listDocuments",
* tags={"document"}, * tags={"document"},
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A list with documents", * description="A list of documents",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Document")) * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Document"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -61,6 +63,29 @@ class DocumentAPIController extends BaseAPIController
* @param DocumentRequest $request * @param DocumentRequest $request
* *
* @return \Illuminate\Http\Response|\Redirect|\Symfony\Component\HttpFoundation\StreamedResponse * @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) public function show(DocumentRequest $request)
{ {
@ -76,11 +101,12 @@ class DocumentAPIController extends BaseAPIController
/** /**
* @SWG\Post( * @SWG\Post(
* path="/documents", * path="/documents",
* tags={"document"},
* summary="Create a document", * summary="Create a document",
* operationId="createDocument",
* tags={"document"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="document",
* @SWG\Schema(ref="#/definitions/Document") * @SWG\Schema(ref="#/definitions/Document")
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -100,4 +126,36 @@ class DocumentAPIController extends BaseAPIController
return $this->itemResponse($document); 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; namespace App\Http\Controllers;
use App\Http\Requests\CreateExpenseRequest;
use App\Http\Requests\ExpenseRequest; use App\Http\Requests\ExpenseRequest;
use App\Http\Requests\CreateExpenseRequest;
use App\Http\Requests\UpdateExpenseRequest; use App\Http\Requests\UpdateExpenseRequest;
use App\Models\Expense; use App\Models\Expense;
use App\Ninja\Repositories\ExpenseRepository; use App\Ninja\Repositories\ExpenseRepository;
@ -28,11 +28,12 @@ class ExpenseApiController extends BaseAPIController
/** /**
* @SWG\Get( * @SWG\Get(
* path="/expenses", * path="/expenses",
* summary="List of expenses", * summary="List expenses",
* operationId="listExpenses",
* tags={"expense"}, * tags={"expense"},
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A list with expenses", * description="A list of expenses",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Expense")) * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Expense"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -51,14 +52,43 @@ class ExpenseApiController extends BaseAPIController
return $this->listResponse($expenses); 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( * @SWG\Post(
* path="/expenses", * path="/expenses",
* summary="Create an expense",
* operationId="createExpense",
* tags={"expense"}, * tags={"expense"},
* summary="Create a expense",
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="expense",
* @SWG\Schema(ref="#/definitions/Expense") * @SWG\Schema(ref="#/definitions/Expense")
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -86,16 +116,23 @@ class ExpenseApiController extends BaseAPIController
/** /**
* @SWG\Put( * @SWG\Put(
* path="/expenses/{expense_id}", * path="/expenses/{expense_id}",
* summary="Update an expense",
* operationId="updateExpense",
* tags={"expense"}, * tags={"expense"},
* summary="Update a expense", * @SWG\Parameter(
* in="path",
* name="expense_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="expense",
* @SWG\Schema(ref="#/definitions/Expense") * @SWG\Schema(ref="#/definitions/Expense")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Update expense", * description="Updated expense",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -122,16 +159,18 @@ class ExpenseApiController extends BaseAPIController
/** /**
* @SWG\Delete( * @SWG\Delete(
* path="/expenses/{expense_id}", * path="/expenses/{expense_id}",
* summary="Delete an expense",
* operationId="deleteExpense",
* tags={"expense"}, * tags={"expense"},
* summary="Delete a expense",
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="path",
* name="body", * name="expense_id",
* @SWG\Schema(ref="#/definitions/Expense") * type="integer",
* required=true
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Delete expense", * description="Deleted expense",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -140,7 +179,7 @@ class ExpenseApiController extends BaseAPIController
* ) * )
* ) * )
*/ */
public function destroy(ExpenseRequest $request) public function destroy(UpdateExpenseRequest $request)
{ {
$expense = $request->entity(); $expense = $request->entity();

View File

@ -2,8 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\ExpenseCategoryRequest;
use App\Http\Requests\CreateExpenseCategoryRequest; use App\Http\Requests\CreateExpenseCategoryRequest;
use App\Http\Requests\UpdateExpenseCategoryRequest; use App\Http\Requests\UpdateExpenseCategoryRequest;
use App\Models\ExpenseCategory;
use App\Ninja\Repositories\ExpenseCategoryRepository; use App\Ninja\Repositories\ExpenseCategoryRepository;
use App\Services\ExpenseCategoryService; use App\Services\ExpenseCategoryService;
use Input; use Input;
@ -22,14 +24,69 @@ class ExpenseCategoryApiController extends BaseAPIController
$this->categoryService = $categoryService; $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( * @SWG\Post(
* path="/expense_categories", * path="/expense_categories",
* tags={"expense_category"},
* summary="Create an expense category", * summary="Create an expense category",
* operationId="createExpenseCategory",
* tags={"expense_category"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="expense_category",
* @SWG\Schema(ref="#/definitions/ExpenseCategory") * @SWG\Schema(ref="#/definitions/ExpenseCategory")
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -53,16 +110,23 @@ class ExpenseCategoryApiController extends BaseAPIController
/** /**
* @SWG\Put( * @SWG\Put(
* path="/expense_categories/{expense_category_id}", * path="/expense_categories/{expense_category_id}",
* tags={"expense_category"},
* summary="Update an 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( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="expense_category",
* @SWG\Schema(ref="#/definitions/ExpenseCategory") * @SWG\Schema(ref="#/definitions/ExpenseCategory")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Update expense category", * description="Updated expense category",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/ExpenseCategory")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/ExpenseCategory"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -77,4 +141,36 @@ class ExpenseCategoryApiController extends BaseAPIController
return $this->itemResponse($category); 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() 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 // Track the referral/campaign code
if (Input::has('rc')) { if (Input::has('rc')) {
Session::set(SESSION_REFERRAL_CODE, Input::get('rc')); Session::set(SESSION_REFERRAL_CODE, Input::get('rc'));

View File

@ -2,57 +2,92 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\ImportService; use App\Services\ImportService;
use App\Jobs\ImportData;
use Exception; use Exception;
use Input; use Input;
use Redirect; use Redirect;
use Session; use Session;
use Utils; use Utils;
use View; use View;
use Auth;
class ImportController extends BaseController class ImportController extends BaseController
{ {
public function __construct(ImportService $importService) public function __construct(ImportService $importService)
{ {
//parent::__construct();
$this->importService = $importService; $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'); $source = Input::get('source');
$files = []; $files = [];
$timestamp = time();
foreach (ImportService::$entityTypes as $entityType) { foreach (ImportService::$entityTypes as $entityType) {
if (Input::file("{$entityType}_file")) { $fileName = $entityType;
$files[$entityType] = Input::file("{$entityType}_file")->getRealPath(); if ($request->hasFile($fileName)) {
if ($source === IMPORT_CSV) { $file = $request->file($fileName);
Session::forget("{$entityType}-data"); $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)) { if (! count($files)) {
Session::flash('error', trans('texts.select_file')); Session::flash('error', trans('texts.select_file'));
return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT); return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT);
} }
try { try {
if ($source === IMPORT_CSV) { if ($source === IMPORT_CSV) {
$data = $this->importService->mapCSV($files); $data = $this->importService->mapCSV($files);
return View::make('accounts.import_map', [
return View::make('accounts.import_map', ['data' => $data]); 'data' => $data,
'timestamp' => $timestamp,
]);
} elseif ($source === IMPORT_JSON) { } elseif ($source === IMPORT_JSON) {
$results = $this->importService->importJSON($files[IMPORT_JSON]); $includeData = filter_var(Input::get('data'), FILTER_VALIDATE_BOOLEAN);
$includeSettings = filter_var(Input::get('settings'), FILTER_VALIDATE_BOOLEAN);
return $this->showResult($results); 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 { } else {
$results = $this->importService->importFiles($source, $files); if (config('queue.default') === 'sync') {
$results = $this->importService->importFiles($source, $files);
return $this->showResult($results); $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) { } catch (Exception $exception) {
Utils::logError($exception); Utils::logError($exception);
Session::flash('error', $exception->getMessage()); Session::flash('error', $exception->getMessage());
@ -63,13 +98,24 @@ class ImportController extends BaseController
public function doImportCSV() public function doImportCSV()
{ {
$map = Input::get('map');
$headers = Input::get('headers');
try { 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) { } catch (Exception $exception) {
Utils::logError($exception); Utils::logError($exception);
Session::flash('error', $exception->getMessage()); Session::flash('error', $exception->getMessage());
@ -77,32 +123,4 @@ class ImportController extends BaseController
return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT); 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; namespace App\Http\Controllers;
use App\Http\Requests\CreateInvoiceAPIRequest;
use App\Http\Requests\InvoiceRequest; use App\Http\Requests\InvoiceRequest;
use App\Http\Requests\CreateInvoiceAPIRequest;
use App\Http\Requests\UpdateInvoiceAPIRequest; use App\Http\Requests\UpdateInvoiceAPIRequest;
use App\Jobs\SendInvoiceEmail; use App\Jobs\SendInvoiceEmail;
use App\Jobs\SendPaymentEmail; use App\Jobs\SendPaymentEmail;
@ -42,11 +42,12 @@ class InvoiceApiController extends BaseAPIController
/** /**
* @SWG\Get( * @SWG\Get(
* path="/invoices", * path="/invoices",
* summary="List of invoices", * summary="List invoices",
* operationId="listInvoices",
* tags={"invoice"}, * tags={"invoice"},
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A list with invoices", * description="A list of invoices",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice")) * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Invoice"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -62,14 +63,25 @@ class InvoiceApiController extends BaseAPIController
->with('invoice_items', 'client') ->with('invoice_items', 'client')
->orderBy('created_at', 'desc'); ->orderBy('created_at', 'desc');
// Filter by invoice number
if ($invoiceNumber = Input::get('invoice_number')) {
$invoices->whereInvoiceNumber($invoiceNumber);
}
return $this->listResponse($invoices); return $this->listResponse($invoices);
} }
/** /**
* @SWG\Get( * @SWG\Get(
* path="/invoices/{invoice_id}", * path="/invoices/{invoice_id}",
* summary="Individual Invoice", * summary="Retrieve an Invoice",
* tags={"invoice"}, * tags={"invoice"},
* @SWG\Parameter(
* in="path",
* name="invoice_id",
* type="integer",
* required=true
* ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A single invoice", * description="A single invoice",
@ -89,11 +101,11 @@ class InvoiceApiController extends BaseAPIController
/** /**
* @SWG\Post( * @SWG\Post(
* path="/invoices", * path="/invoices",
* tags={"invoice"},
* summary="Create an invoice", * summary="Create an invoice",
* tags={"invoice"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="invoice",
* @SWG\Schema(ref="#/definitions/Invoice") * @SWG\Schema(ref="#/definitions/Invoice")
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -297,27 +309,38 @@ class InvoiceApiController extends BaseAPIController
{ {
$invoice = $request->entity(); $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(); $headers = Utils::getApiHeaders();
$response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT);
return Response::make($response, 200, $headers); return Response::make($response, 200, $headers);
} }
/** /**
* @SWG\Put( * @SWG\Put(
* path="/invoices", * path="/invoices/{invoice_id}",
* tags={"invoice"},
* summary="Update an invoice", * summary="Update an invoice",
* tags={"invoice"},
* @SWG\Parameter(
* in="path",
* name="invoice_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="invoice",
* @SWG\Schema(ref="#/definitions/Invoice") * @SWG\Schema(ref="#/definitions/Invoice")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Update invoice", * description="Updated invoice",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -352,17 +375,18 @@ class InvoiceApiController extends BaseAPIController
/** /**
* @SWG\Delete( * @SWG\Delete(
* path="/invoices", * path="/invoices/{invoice_id}",
* tags={"invoice"},
* summary="Delete an invoice", * summary="Delete an invoice",
* tags={"invoice"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="path",
* name="body", * name="invoice_id",
* @SWG\Schema(ref="#/definitions/Invoice") * type="integer",
* required=true
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Delete invoice", * description="Deleted invoice",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Invoice"))
* ), * ),
* @SWG\Response( * @SWG\Response(

View File

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

View File

@ -302,6 +302,7 @@ class OnlinePaymentController extends BaseController
} }
Auth::onceUsingId($account->users[0]->id); Auth::onceUsingId($account->users[0]->id);
$account->loadLocalizationSettings();
$product = Product::scope(Input::get('product_id'))->first(); $product = Product::scope(Input::get('product_id'))->first();
if (! $product) { if (! $product) {
@ -330,6 +331,8 @@ class OnlinePaymentController extends BaseController
$data = [ $data = [
'currency_id' => $account->currency_id, 'currency_id' => $account->currency_id,
'contact' => Input::all(), 'contact' => Input::all(),
'custom_value1' => Input::get('custom_client1'),
'custom_value2' => Input::get('custom_client2'),
]; ];
$client = $clientRepo->save($data, $client); $client = $clientRepo->save($data, $client);
} }
@ -343,6 +346,8 @@ class OnlinePaymentController extends BaseController
'start_date' => Input::get('start_date', date('Y-m-d')), 'start_date' => Input::get('start_date', date('Y-m-d')),
'tax_rate1' => $account->default_tax_rate ? $account->default_tax_rate->rate : 0, 'tax_rate1' => $account->default_tax_rate ? $account->default_tax_rate->rate : 0,
'tax_name1' => $account->default_tax_rate ? $account->default_tax_rate->name : '', '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' => [[ 'invoice_items' => [[
'product_key' => $product->product_key, 'product_key' => $product->product_key,
'notes' => $product->notes, 'notes' => $product->notes,
@ -350,6 +355,8 @@ class OnlinePaymentController extends BaseController
'qty' => 1, 'qty' => 1,
'tax_rate1' => $product->default_tax_rate ? $product->default_tax_rate->rate : 0, 'tax_rate1' => $product->default_tax_rate ? $product->default_tax_rate->rate : 0,
'tax_name1' => $product->default_tax_rate ? $product->default_tax_rate->name : '', '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); $invoice = $invoiceService->save($data);
@ -364,9 +371,15 @@ class OnlinePaymentController extends BaseController
} }
if ($gatewayTypeAlias) { if ($gatewayTypeAlias) {
return redirect()->to($invitation->getLink('payment') . "/{$gatewayTypeAlias}"); $link = $invitation->getLink('payment') . "/{$gatewayTypeAlias}";
} else { } 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; namespace App\Http\Controllers;
use App\Http\Requests\PaymentRequest;
use App\Http\Requests\CreatePaymentAPIRequest; use App\Http\Requests\CreatePaymentAPIRequest;
use App\Http\Requests\UpdatePaymentRequest; use App\Http\Requests\UpdatePaymentRequest;
use App\Models\Invoice; use App\Models\Invoice;
@ -28,11 +29,12 @@ class PaymentApiController extends BaseAPIController
/** /**
* @SWG\Get( * @SWG\Get(
* path="/payments", * path="/payments",
* summary="List payments",
* operationId="listPayments",
* tags={"payment"}, * tags={"payment"},
* summary="List of payments",
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A list with payments", * description="A list of payments",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Payment")) * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Payment"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -52,18 +54,20 @@ class PaymentApiController extends BaseAPIController
} }
/** /**
* @SWG\Put( * @SWG\Get(
* path="/payments/{payment_id", * path="/payments/{payment_id}",
* summary="Update a payment", * summary="Retrieve a payment",
* operationId="getPayment",
* tags={"payment"}, * tags={"payment"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="path",
* name="body", * name="payment_id",
* @SWG\Schema(ref="#/definitions/Payment") * type="integer",
* required=true
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Update payment", * description="A single payment",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Payment"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -71,30 +75,21 @@ class PaymentApiController extends BaseAPIController
* description="an ""unexpected"" error" * description="an ""unexpected"" error"
* ) * )
* ) * )
*
* @param mixed $publicId
*/ */
public function update(UpdatePaymentRequest $request, $publicId) public function show(PaymentRequest $request)
{ {
if ($request->action) { return $this->itemResponse($request->entity());
return $this->handleAction($request);
}
$data = $request->input();
$data['public_id'] = $publicId;
$payment = $this->paymentRepo->save($data, $request->entity());
return $this->itemResponse($payment);
} }
/** /**
* @SWG\Post( * @SWG\Post(
* path="/payments", * path="/payments",
* summary="Create a payment", * summary="Create a payment",
* operationId="createPayment",
* tags={"payment"}, * tags={"payment"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="payment",
* @SWG\Schema(ref="#/definitions/Payment") * @SWG\Schema(ref="#/definitions/Payment")
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -123,18 +118,63 @@ class PaymentApiController extends BaseAPIController
} }
/** /**
* @SWG\Delete( * @SWG\Put(
* path="/payments/{payment_id}", * path="/payments/{payment_id}",
* summary="Delete a payment", * summary="Update a payment",
* operationId="updatePayment",
* tags={"payment"}, * tags={"payment"},
* @SWG\Parameter( * @SWG\Parameter(
* in="path",
* name="payment_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="payment",
* @SWG\Schema(ref="#/definitions/Payment") * @SWG\Schema(ref="#/definitions/Payment")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * 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\Schema(type="object", @SWG\Items(ref="#/definitions/Payment"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -147,7 +187,7 @@ class PaymentApiController extends BaseAPIController
{ {
$payment = $request->entity(); $payment = $request->entity();
$this->clientRepo->delete($payment); $this->paymentRepo->delete($payment);
return $this->itemResponse($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\PaymentRequest;
use App\Http\Requests\UpdatePaymentRequest; use App\Http\Requests\UpdatePaymentRequest;
use App\Models\Client; use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Ninja\Datatables\PaymentDatatable; use App\Ninja\Datatables\PaymentDatatable;
use App\Ninja\Mailers\ContactMailer; use App\Ninja\Mailers\ContactMailer;
@ -89,7 +90,7 @@ class PaymentController extends BaseController
{ {
$invoices = Invoice::scope() $invoices = Invoice::scope()
->invoices() ->invoices()
->where('invoices.balance', '>', 0) ->where('invoices.balance', '!=', 0)
->with('client', 'invoice_status') ->with('client', 'invoice_status')
->orderBy('invoice_number')->get(); ->orderBy('invoice_number')->get();
@ -180,17 +181,28 @@ class PaymentController extends BaseController
{ {
// check payment has been marked sent // check payment has been marked sent
$request->invoice->markSentIfUnsent(); $request->invoice->markSentIfUnsent();
$input = $request->input(); $input = $request->input();
$input['invoice_id'] = Invoice::getPrivateId($input['invoice']); $amount = Utils::parseFloat($input['amount']);
$input['client_id'] = Client::getPrivateId($input['client']); $credit = false;
$payment = $this->paymentRepo->save($input);
// 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')) { if (Input::get('email_receipt')) {
$this->contactMailer->sendPaymentConfirmation($payment); $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 { } 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'); return redirect()->to($payment->client->getRoute() . '#payments');

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\ProductRequest;
use App\Http\Requests\CreateProductRequest; use App\Http\Requests\CreateProductRequest;
use App\Http\Requests\UpdateProductRequest; use App\Http\Requests\UpdateProductRequest;
use App\Models\Product; use App\Models\Product;
@ -37,11 +38,12 @@ class ProductApiController extends BaseAPIController
/** /**
* @SWG\Get( * @SWG\Get(
* path="/products", * path="/products",
* summary="List of products", * summary="List products",
* operationId="listProducts",
* tags={"product"}, * tags={"product"},
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A list with products", * description="A list of products",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Product")) * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Product"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -59,11 +61,40 @@ class ProductApiController extends BaseAPIController
return $this->listResponse($products); 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( * @SWG\Post(
* path="/products", * path="/products",
* tags={"product"},
* summary="Create a product", * summary="Create a product",
* operationId="createProduct",
* tags={"product"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="body",
@ -90,16 +121,23 @@ class ProductApiController extends BaseAPIController
/** /**
* @SWG\Put( * @SWG\Put(
* path="/products/{product_id}", * path="/products/{product_id}",
* tags={"product"},
* summary="Update a product", * summary="Update a product",
* operationId="updateProduct",
* tags={"product"},
* @SWG\Parameter(
* in="path",
* name="product_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="product",
* @SWG\Schema(ref="#/definitions/Product") * @SWG\Schema(ref="#/definitions/Product")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Update product", * description="Updated product",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Product")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Product"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -122,4 +160,36 @@ class ProductApiController extends BaseAPIController
return $this->itemResponse($product); 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\Product;
use App\Models\TaxRate; use App\Models\TaxRate;
use App\Ninja\Datatables\ProductDatatable; use App\Ninja\Datatables\ProductDatatable;
use App\Ninja\Repositories\ProductRepository;
use App\Services\ProductService; use App\Services\ProductService;
use Auth; use Auth;
use Input; use Input;
@ -24,16 +25,22 @@ class ProductController extends BaseController
*/ */
protected $productService; protected $productService;
/**
* @var ProductRepository
*/
protected $productRepo;
/** /**
* ProductController constructor. * ProductController constructor.
* *
* @param ProductService $productService * @param ProductService $productService
*/ */
public function __construct(ProductService $productService) public function __construct(ProductService $productService, ProductRepository $productRepo)
{ {
//parent::__construct(); //parent::__construct();
$this->productService = $productService; $this->productService = $productService;
$this->productRepo = $productRepo;
} }
/** /**
@ -137,11 +144,7 @@ class ProductController extends BaseController
$product = Product::createNew(); $product = Product::createNew();
} }
$product->product_key = trim(Input::get('product_key')); $this->productRepo->save(Input::all(), $product);
$product->notes = trim(Input::get('notes'));
$product->cost = trim(Input::get('cost'));
$product->fill(Input::all());
$product->save();
$message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product'); $message = $productPublicId ? trans('texts.updated_product') : trans('texts.created_product');
Session::flash('message', $message); Session::flash('message', $message);

View File

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

View File

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

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\TaskRequest;
use App\Http\Requests\UpdateTaskRequest; use App\Http\Requests\UpdateTaskRequest;
use App\Models\Task; use App\Models\Task;
use App\Ninja\Repositories\TaskRepository; use App\Ninja\Repositories\TaskRepository;
@ -26,11 +27,12 @@ class TaskApiController extends BaseAPIController
/** /**
* @SWG\Get( * @SWG\Get(
* path="/tasks", * path="/tasks",
* summary="List tasks",
* operationId="listTasks",
* tags={"task"}, * tags={"task"},
* summary="List of tasks",
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A list with tasks", * description="A list of tasks",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Task")) * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Task"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -49,14 +51,43 @@ class TaskApiController extends BaseAPIController
return $this->listResponse($tasks); 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( * @SWG\Post(
* path="/tasks", * path="/tasks",
* tags={"task"},
* summary="Create a task", * summary="Create a task",
* operationId="createTask",
* tags={"task"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="task",
* @SWG\Schema(ref="#/definitions/Task") * @SWG\Schema(ref="#/definitions/Task")
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -90,9 +121,16 @@ class TaskApiController extends BaseAPIController
/** /**
* @SWG\Put( * @SWG\Put(
* path="/task/{task_id}", * path="/tasks/{task_id}",
* tags={"task"},
* summary="Update a task", * summary="Update a task",
* operationId="updateTask",
* tags={"task"},
* @SWG\Parameter(
* in="path",
* name="task_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="body",
@ -117,4 +155,36 @@ class TaskApiController extends BaseAPIController
return $this->itemResponse($task); 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) public function store(CreateTaskRequest $request)
{ {
return $this->save(); return $this->save($request);
} }
/** /**
@ -202,7 +202,7 @@ class TaskController extends BaseController
{ {
$task = $request->entity(); $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 * @return \Illuminate\Http\RedirectResponse
*/ */
private function save($publicId = null) private function save($request, $publicId = null)
{ {
$action = Input::get('action'); $action = Input::get('action');
@ -230,7 +230,7 @@ class TaskController extends BaseController
return self::bulk(); return self::bulk();
} }
$task = $this->taskRepo->save($publicId, Input::all()); $task = $this->taskRepo->save($publicId, $request->input());
if ($publicId) { if ($publicId) {
Session::flash('message', trans('texts.updated_task')); Session::flash('message', trans('texts.updated_task'));

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\TaxRateRequest;
use App\Http\Requests\CreateTaxRateRequest; use App\Http\Requests\CreateTaxRateRequest;
use App\Http\Requests\UpdateTaxRateRequest; use App\Http\Requests\UpdateTaxRateRequest;
use App\Models\TaxRate; use App\Models\TaxRate;
@ -34,11 +35,12 @@ class TaxRateApiController extends BaseAPIController
/** /**
* @SWG\Get( * @SWG\Get(
* path="/tax_rates", * path="/tax_rates",
* summary="List of tax rates", * summary="List tax rates",
* operationId="listTaxRates",
* tags={"tax_rate"}, * tags={"tax_rate"},
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="A list with tax rates", * description="A list of tax rates",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/TaxRate")) * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/TaxRate"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -56,14 +58,43 @@ class TaxRateApiController extends BaseAPIController
return $this->listResponse($taxRates); 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( * @SWG\Post(
* path="/tax_rates", * path="/tax_rates",
* tags={"tax_rate"},
* summary="Create a tax rate", * summary="Create a tax rate",
* operationId="createTaxRate",
* tags={"tax_rate"},
* @SWG\Parameter( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="tax_rate",
* @SWG\Schema(ref="#/definitions/TaxRate") * @SWG\Schema(ref="#/definitions/TaxRate")
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -87,16 +118,23 @@ class TaxRateApiController extends BaseAPIController
/** /**
* @SWG\Put( * @SWG\Put(
* path="/tax_rates/{tax_rate_id}", * path="/tax_rates/{tax_rate_id}",
* tags={"tax_rate"},
* summary="Update a 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( * @SWG\Parameter(
* in="body", * in="body",
* name="body", * name="tax_rate",
* @SWG\Schema(ref="#/definitions/TaxRate") * @SWG\Schema(ref="#/definitions/TaxRate")
* ), * ),
* @SWG\Response( * @SWG\Response(
* response=200, * response=200,
* description="Update tax rate", * description="Updated tax rate",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/TaxRate")) * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/TaxRate"))
* ), * ),
* @SWG\Response( * @SWG\Response(
@ -119,4 +157,36 @@ class TaxRateApiController extends BaseAPIController
return $this->itemResponse($taxRate); 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 { } else {
$token = AccountToken::createNew(); $token = AccountToken::createNew();
$token->name = trim(Input::get('name')); $token->name = trim(Input::get('name'));
$token->token = str_random(RANDOM_KEY_LENGTH); $token->token = strtolower(str_random(RANDOM_KEY_LENGTH));
} }
$token->save(); $token->save();

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\UserRequest;
use App\Http\Requests\CreateUserRequest; use App\Http\Requests\CreateUserRequest;
use App\Http\Requests\UpdateUserRequest; use App\Http\Requests\UpdateUserRequest;
use App\Models\User; use App\Models\User;
@ -25,22 +26,117 @@ class UserApiController extends BaseAPIController
$this->userRepo = $userRepo; $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() public function index()
{ {
$users = User::whereAccountId(Auth::user()->account_id) $users = User::whereAccountId(Auth::user()->account_id)
->withTrashed() ->withTrashed()
->orderBy('created_at', 'desc'); ->orderBy('created_at', 'desc');
return $this->listResponse($users); 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) public function store(CreateUserRequest $request)
{ {
return $this->save($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) public function update(UpdateUserRequest $request, $userPublicId)
{ {
$user = Auth::user(); $user = Auth::user();
@ -66,4 +162,36 @@ class UserApiController extends BaseAPIController
return $this->response($data); 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->username = trim(Input::get('email'));
$user->email = trim(Input::get('email')); $user->email = trim(Input::get('email'));
$user->registered = true; $user->registered = true;
$user->password = str_random(RANDOM_KEY_LENGTH); $user->password = strtolower(str_random(RANDOM_KEY_LENGTH));
$user->confirmation_code = str_random(RANDOM_KEY_LENGTH); $user->confirmation_code = strtolower(str_random(RANDOM_KEY_LENGTH));
$user->public_id = $lastUser->public_id + 1; $user->public_id = $lastUser->public_id + 1;
if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) { if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) {
$user->is_admin = boolval(Input::get('is_admin')); $user->is_admin = boolval(Input::get('is_admin'));
@ -210,7 +210,7 @@ class UserController extends BaseController
$user->save(); $user->save();
if (! $user->confirmed) { if (! $user->confirmed && Input::get('action') === 'email') {
$this->userMailer->sendConfirmation($user, Auth::user()); $this->userMailer->sendConfirmation($user, Auth::user());
$message = trans('texts.sent_invite'); $message = trans('texts.sent_invite');
} else { } else {

View File

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

View File

@ -1,7 +1,7 @@
<?php <?php
namespace App\Http; namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel class Kernel extends HttpKernel
@ -34,6 +34,6 @@ class Kernel extends HttpKernel
'permissions.required' => 'App\Http\Middleware\PermissionsRequired', 'permissions.required' => 'App\Http\Middleware\PermissionsRequired',
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated', 'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
'api' => 'App\Http\Middleware\ApiCheck', '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_key = session('contact_key');
} }
$contact = false;
if ($contact_key) { if ($contact_key) {
$contact = $this->getContact($contact_key); $contact = $this->getContact($contact_key);
} elseif ($invitation = $this->getInvitation($request->invitation_key)) { } elseif ($invitation = $this->getInvitation($request->invitation_key)) {
$contact = $invitation->contact; $contact = $invitation->contact;
Session::put('contact_key', $contact->contact_key); Session::put('contact_key', $contact->contact_key);
} else { }
if (! $contact) {
return \Redirect::to('client/sessionexpired'); return \Redirect::to('client/sessionexpired');
} }
$account = $contact->account; $account = $contact->account;
@ -113,6 +115,7 @@ class Authenticate
// check for extra params at end of value (from website feature) // check for extra params at end of value (from website feature)
list($key) = explode('&', $key); list($key) = explode('&', $key);
$key = substr($key, 0, RANDOM_KEY_LENGTH);
$invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first(); $invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first();
if ($invitation && ! $invitation->is_deleted) { 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) { if (! $this->invoice_id || ! $this->amount) {
return [ return [
'invoice_id' => 'required|numeric|min:1', '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() ->invoices()
->firstOrFail(); ->firstOrFail();
$this->merge([
'invoice_id' => $invoice->id,
'client_id' => $invoice->client->id,
]);
$rules = [ $rules = [
'client' => 'required', // TODO: change to client_id once views are updated 'client' => 'required', // TODO: change to client_id once views are updated
'invoice' => 'required', // TODO: change to invoice_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', 'payment_date' => 'required',
]; ];

View File

@ -3,6 +3,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Models\ExpenseCategory; use App\Models\ExpenseCategory;
use App\Models\Vendor;
class ExpenseRequest extends EntityRequest class ExpenseRequest extends EntityRequest
{ {
@ -24,11 +25,37 @@ class ExpenseRequest extends EntityRequest
{ {
$input = $this->all(); $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); $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(); return $this->all();
} }
} }

View File

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

View File

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

View File

@ -2,7 +2,33 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Models\Client;
use App\Models\Project;
class TaskRequest extends EntityRequest class TaskRequest extends EntityRequest
{ {
protected $entityType = ENTITY_TASK; 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'); Route::post('/reseller_stats', 'AppController@stats');
} }
if (Utils::isTravis()) {
Route::get('/check_data', 'AppController@checkData');
}
Route::group(['middleware' => 'auth:user'], function () { Route::group(['middleware' => 'auth:user'], function () {
Route::get('dashboard', 'DashboardController@index'); Route::get('dashboard', 'DashboardController@index');
Route::get('dashboard_chart_data/{group_by}/{start_date}/{end_date}/{currency_id}/{include_expenses}', 'DashboardController@chartData'); 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::get('settings/user_details', 'AccountController@showUserDetails');
Route::post('settings/user_details', 'AccountController@saveUserDetails'); 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::post('users/change_password', 'UserController@changePassword');
Route::resource('clients', 'ClientController'); Route::resource('clients', 'ClientController');
@ -253,6 +257,7 @@ Route::group([
Route::post('settings/change_plan', 'AccountController@changePlan'); Route::post('settings/change_plan', 'AccountController@changePlan');
Route::post('settings/cancel_account', 'AccountController@cancelAccount'); Route::post('settings/cancel_account', 'AccountController@cancelAccount');
Route::post('settings/purge_data', 'AccountController@purgeData');
Route::post('settings/company_details', 'AccountController@updateDetails'); Route::post('settings/company_details', 'AccountController@updateDetails');
Route::post('settings/{section?}', 'AccountController@doSection'); Route::post('settings/{section?}', 'AccountController@doSection');
@ -303,12 +308,11 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function () {
Route::get('accounts', 'AccountApiController@show'); Route::get('accounts', 'AccountApiController@show');
Route::put('accounts', 'AccountApiController@update'); Route::put('accounts', 'AccountApiController@update');
Route::resource('clients', 'ClientApiController'); Route::resource('clients', 'ClientApiController');
Route::resource('contacts', 'ContactApiController');
Route::get('quotes', 'QuoteApiController@index'); Route::get('quotes', 'QuoteApiController@index');
Route::get('invoices', 'InvoiceApiController@index');
Route::get('download/{invoice_id}', 'InvoiceApiController@download'); Route::get('download/{invoice_id}', 'InvoiceApiController@download');
Route::resource('invoices', 'InvoiceApiController'); Route::resource('invoices', 'InvoiceApiController');
Route::resource('payments', 'PaymentApiController'); Route::resource('payments', 'PaymentApiController');
Route::get('tasks', 'TaskApiController@index');
Route::resource('tasks', 'TaskApiController'); Route::resource('tasks', 'TaskApiController');
Route::post('hooks', 'IntegrationController@subscribe'); Route::post('hooks', 'IntegrationController@subscribe');
Route::post('email_invoice', 'InvoiceApiController@emailInvoice'); Route::post('email_invoice', 'InvoiceApiController@emailInvoice');
@ -359,6 +363,9 @@ Route::get('/feed', function () {
Route::get('/comments/feed', function () { Route::get('/comments/feed', function () {
return Redirect::to(NINJA_WEB_URL.'/comments/feed', 301); return Redirect::to(NINJA_WEB_URL.'/comments/feed', 301);
}); });
Route::get('/terms', function () {
return Redirect::to(NINJA_WEB_URL.'/terms', 301);
});
/* /*
if (Utils::isNinjaDev()) 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; protected $reminder;
/**
* @var string
*/
protected $pdfString;
/** /**
* @var array * @var array
*/ */
@ -44,11 +39,10 @@ class SendInvoiceEmail extends Job implements ShouldQueue
* @param bool $reminder * @param bool $reminder
* @param mixed $pdfString * @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->invoice = $invoice;
$this->reminder = $reminder; $this->reminder = $reminder;
$this->pdfString = $pdfString;
$this->template = $template; $this->template = $template;
} }
@ -59,7 +53,7 @@ class SendInvoiceEmail extends Job implements ShouldQueue
*/ */
public function handle(ContactMailer $mailer) 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() : '', 'user_name' => Auth::check() ? Auth::user()->getDisplayName() : '',
'method' => Request::method(), 'method' => Request::method(),
'url' => Input::get('url', Request::url()), 'url' => Input::get('url', Request::url()),
'previous' => url()->previous(),
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'ip' => Request::getClientIp(), 'ip' => Request::getClientIp(),
'count' => Session::get('error_count', 0), 'count' => Session::get('error_count', 0),

View File

@ -5,7 +5,9 @@ namespace App\Listeners;
use App\Events\UserLoggedIn; use App\Events\UserLoggedIn;
use App\Events\UserSignedUp; use App\Events\UserSignedUp;
use App\Libraries\HistoryUtils; use App\Libraries\HistoryUtils;
use App\Models\Gateway;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
use Utils;
use Auth; use Auth;
use Carbon; use Carbon;
use Session; use Session;
@ -65,5 +67,10 @@ class HandleUserLoggedIn
} elseif ($account->isLogoTooLarge()) { } elseif ($account->isLogoTooLarge()) {
Session::flash('warning', trans('texts.logo_too_large', ['size' => $account->getLogoSize() . 'KB'])); 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) 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); Utils::logError($exception->exception);
} }
} }

View File

@ -48,51 +48,131 @@ class Account extends Eloquent
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'timezone_id',
'date_format_id',
'datetime_format_id',
'currency_id',
'name', 'name',
'id_number',
'vat_number',
'work_email',
'website',
'work_phone',
'address1', 'address1',
'address2', 'address2',
'city', 'city',
'state', 'state',
'postal_code', 'postal_code',
'country_id', 'country_id',
'size_id', 'invoice_terms',
'industry_id',
'email_footer', 'email_footer',
'timezone_id', 'industry_id',
'date_format_id', 'size_id',
'datetime_format_id',
'currency_id',
'language_id',
'military_time',
'invoice_taxes', 'invoice_taxes',
'invoice_item_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', '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', 'default_tax_rate_id',
'enable_second_tax_rate', 'recurring_hour',
'include_item_taxes_inline', 'invoice_number_pattern',
'start_of_week', 'quote_number_pattern',
'financial_year_start', 'quote_terms',
'enable_client_portal', 'email_design_id',
'enable_client_portal_dashboard', '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', 'enable_portal_password',
'send_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', 'enable_buy_now_buttons',
'include_item_taxes_inline',
'financial_year_start',
'enabled_modules',
'enabled_dashboard_sections',
'show_accept_invoice_terms', 'show_accept_invoice_terms',
'show_accept_quote_terms', 'show_accept_quote_terms',
'require_invoice_signature', 'require_invoice_signature',
'require_quote_signature', 'require_quote_signature',
'pdf_email_attachment', 'client_number_prefix',
'document_email_attachment', 'client_number_counter',
'email_design_id', 'client_number_pattern',
'enable_email_markup',
'domain_id',
'payment_terms', 'payment_terms',
'reset_counter_frequency_id',
'payment_type_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 $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 * @return \Illuminate\Database\Eloquent\Relations\HasMany
*/ */
@ -402,6 +498,22 @@ class Account extends Eloquent
return $user->getDisplayName(); 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 * @return string
*/ */
@ -887,6 +999,8 @@ class Account extends Eloquent
$invoice->start_date = Utils::today(); $invoice->start_date = Utils::today();
$invoice->invoice_design_id = $this->invoice_design_id; $invoice->invoice_design_id = $this->invoice_design_id;
$invoice->client_id = $clientId; $invoice->client_id = $clientId;
$invoice->custom_taxes1 = $this->custom_invoice_taxes1;
$invoice->custom_taxes2 = $this->custom_invoice_taxes2;
if ($entityType === ENTITY_RECURRING_INVOICE) { if ($entityType === ENTITY_RECURRING_INVOICE) {
$invoice->invoice_number = microtime(true); $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; namespace App\Models;
use Utils;
/** /**
* Class AccountGatewaySettings. * Class AccountGatewaySettings.
*/ */
@ -12,6 +14,18 @@ class AccountGatewaySettings extends EntityModel
*/ */
protected $dates = ['updated_at']; 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 * @var bool
*/ */
@ -29,4 +43,29 @@ class AccountGatewaySettings extends EntityModel
{ {
// to Disable created_at // 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', 'language_id',
'payment_terms', 'payment_terms',
'website', 'website',
'invoice_number_counter',
'quote_number_counter',
]; ];
/** /**
@ -136,7 +138,7 @@ class Client extends EntityModel
'email' => 'email', 'email' => 'email',
'mobile|phone' => 'phone', 'mobile|phone' => 'phone',
'name|organization' => 'name', 'name|organization' => 'name',
'street2|address2' => 'address2', 'apt|street2|address2' => 'address2',
'street|address|address1' => 'address1', 'street|address|address1' => 'address1',
'city' => 'city', 'city' => 'city',
'state|province' => 'state', 'state|province' => 'state',
@ -145,7 +147,7 @@ class Client extends EntityModel
'note' => 'notes', 'note' => 'notes',
'site|website' => 'website', 'site|website' => 'website',
'vat' => 'vat_number', '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')) { if (isset($data['contact_key']) && $this->account->account_key == env('NINJA_LICENSE_ACCOUNT_KEY')) {
$contact->contact_key = $data['contact_key']; $contact->contact_key = $data['contact_key'];
} else { } 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; 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) public function getContactKeyAttribute($contact_key)
{ {
if (empty($contact_key) && $this->id) { 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]); 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'; protected $presenter = 'App\Ninja\Presenters\CreditPresenter';
/**
* @var array
*/
protected $fillable = [
'public_notes',
'private_notes',
];
/** /**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/ */

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ use App\Events\QuoteWasCreated;
use App\Events\QuoteWasUpdated; use App\Events\QuoteWasUpdated;
use App\Libraries\CurlUtils; use App\Libraries\CurlUtils;
use App\Models\Activity; use App\Models\Activity;
use App\Models\Traits\ChargesFees;
use DateTime; use DateTime;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
@ -23,6 +24,7 @@ class Invoice extends EntityModel implements BalanceAffecting
{ {
use PresentableTrait; use PresentableTrait;
use OwnedByClientTrait; use OwnedByClientTrait;
use ChargesFees;
use SoftDeletes { use SoftDeletes {
SoftDeletes::trashed as parentTrashed; SoftDeletes::trashed as parentTrashed;
} }
@ -62,9 +64,10 @@ class Invoice extends EntityModel implements BalanceAffecting
*/ */
public static $patternFields = [ public static $patternFields = [
'counter', 'counter',
'custom1', 'clientCounter',
'custom2', 'clientIdNumber',
'idNumber', 'clientCustom1',
'clientCustom2',
'userId', 'userId',
'year', 'year',
'date:', 'date:',
@ -539,7 +542,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function updatePaidStatus($save = true) public function updatePaidStatus($save = true)
{ {
$statusId = false; $statusId = false;
if ($this->amount > 0 && $this->balance == 0) { if ($this->amount != 0 && $this->balance == 0) {
$statusId = INVOICE_STATUS_PAID; $statusId = INVOICE_STATUS_PAID;
} elseif ($this->balance > 0 && $this->balance < $this->amount) { } elseif ($this->balance > 0 && $this->balance < $this->amount) {
$statusId = INVOICE_STATUS_PARTIAL; $statusId = INVOICE_STATUS_PARTIAL;
@ -573,6 +576,13 @@ class Invoice extends EntityModel implements BalanceAffecting
return; return;
} }
$balanceAdjustment = floatval($balanceAdjustment);
$partial = floatval($partial);
if (! $balanceAdjustment && $this->partial == $partial) {
return;
}
$this->balance = $this->balance + $balanceAdjustment; $this->balance = $this->balance + $balanceAdjustment;
if ($this->partial > 0) { if ($this->partial > 0) {
@ -580,6 +590,13 @@ class Invoice extends EntityModel implements BalanceAffecting
} }
$this->save(); $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() 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) public static function calcStatusLabel($status, $class, $entityType, $quoteInvoiceId)
@ -752,7 +769,16 @@ class Invoice extends EntityModel implements BalanceAffecting
*/ */
public function getRequestedAmount() 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', 'page_size',
'include_item_taxes_inline', 'include_item_taxes_inline',
'invoice_fields', 'invoice_fields',
'show_currency_code',
]); ]);
foreach ($this->invoice_items as $invoiceItem) { foreach ($this->invoice_items as $invoiceItem) {
@ -1231,6 +1258,10 @@ class Invoice extends EntityModel implements BalanceAffecting
return false; return false;
} }
if (Utils::isTravis()) {
return false;
}
$invitation = $this->invitations[0]; $invitation = $this->invitations[0];
$link = $invitation->getLink('view', true); $link = $invitation->getLink('view', true);
$pdfString = false; $pdfString = false;
@ -1238,26 +1269,35 @@ class Invoice extends EntityModel implements BalanceAffecting
try { try {
if (env('PHANTOMJS_BIN_PATH')) { if (env('PHANTOMJS_BIN_PATH')) {
$pdfString = CurlUtils::phantom('GET', $link . '?phantomjs=true&phantomjs_secret=' . env('PHANTOMJS_SECRET')); $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); $pdfString = strip_tags($pdfString);
} catch (\Exception $exception) { } catch (\Exception $exception) {
Utils::logError("PhantomJS - Failed to create pdf: {$exception->getMessage()}"); Utils::logError("PhantomJS - Failed to load: {$exception->getMessage()}");
return false; return false;
} }
if (! $pdfString || strlen($pdfString) < 200) { if (! $pdfString || strlen($pdfString) < 200) {
Utils::logError("PhantomJS - Failed to create pdf: {$pdfString}"); Utils::logError("PhantomJS - Invalid response: {$pdfString}");
return false; 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) { Invoice::creating(function ($invoice) {
if (! $invoice->is_recurring) { if (! $invoice->is_recurring && $invoice->amount >= 0) {
$invoice->account->incrementCounter($invoice); $invoice->account->incrementCounter($invoice);
} }
}); });

View File

@ -39,6 +39,7 @@ class InvoiceItem extends EntityModel
'tax_rate1', 'tax_rate1',
'tax_name2', 'tax_name2',
'tax_rate2', 'tax_rate2',
'invoice_item_type_id',
]; ];
/** /**
@ -72,4 +73,28 @@ class InvoiceItem extends EntityModel
{ {
return $this->belongsTo('App\Models\Account'); 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) 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) >= ' . $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; 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); $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)) { if ($entity->isEntityType(ENTITY_CLIENT)) {
$check = Client::scope(false, $this->id)->whereIdNumber($number)->withTrashed()->first(); $check = Client::scope(false, $this->id)->whereIdNumber($number)->withTrashed()->first();
} else { } else {
@ -66,10 +70,6 @@ trait GeneratesNumbers
} }
} }
if ($entity->recurring_invoice_id) {
$number = $this->recurring_invoice_number_prefix . $number;
}
return $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; $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); $pattern = str_replace($search, $replace, $pattern);
$pattern = $this->getClientInvoiceNumber($pattern, $entity);
if ($entity->client_id) {
$pattern = $this->getClientInvoiceNumber($pattern, $entity);
}
return $pattern; return $pattern;
} }
@ -183,7 +180,7 @@ trait GeneratesNumbers
*/ */
private function getClientInvoiceNumber($pattern, $invoice) private function getClientInvoiceNumber($pattern, $invoice)
{ {
if (! $invoice->client) { if (! $invoice->client_id) {
return $pattern; return $pattern;
} }
@ -191,12 +188,21 @@ trait GeneratesNumbers
'{$custom1}', '{$custom1}',
'{$custom2}', '{$custom2}',
'{$idNumber}', '{$idNumber}',
'{$clientCustom1}',
'{$clientCustom2}',
'{$clientIdNumber}',
'{$clientCounter}',
]; ];
$replace = [ $replace = [
$invoice->client->custom_value1, $invoice->client->custom_value1,
$invoice->client->custom_value2, $invoice->client->custom_value2,
$invoice->client->id_number, $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); return str_replace($search, $replace, $pattern);
@ -225,7 +231,9 @@ trait GeneratesNumbers
*/ */
public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE) 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); return $this->getNextNumber($invoice);
} }
@ -239,17 +247,87 @@ trait GeneratesNumbers
if ($this->client_number_counter) { if ($this->client_number_counter) {
$this->client_number_counter += 1; $this->client_number_counter += 1;
} }
} elseif ($entity->isType(INVOICE_TYPE_QUOTE) && ! $this->share_counter) { $this->save();
$this->quote_number_counter += 1; return;
} else {
$this->invoice_number_counter += 1;
} }
$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() 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.client_name',
'client.id_number', 'client.id_number',
'client.vat_number', 'client.vat_number',
'client.website',
'client.work_phone',
'client.address1', 'client.address1',
'client.address2', 'client.address2',
'client.city_state_postal', 'client.city_state_postal',
'client.postal_city_state', 'client.postal_city_state',
'client.country', 'client.country',
'client.contact_name',
'client.email', 'client.email',
'client.phone', 'client.phone',
'client.contact_name',
'client.custom_value1', 'client.custom_value1',
'client.custom_value2', 'client.custom_value2',
'.blank', '.blank',
@ -138,6 +140,8 @@ trait PresentsInvoice
list($entityType, $fieldName) = explode('.', $field); list($entityType, $fieldName) = explode('.', $field);
if (substr($fieldName, 0, 6) == 'custom') { if (substr($fieldName, 0, 6) == 'custom') {
$fields[$section][$field] = $labels[$field]; $fields[$section][$field] = $labels[$field];
} elseif (in_array($field, ['client.phone', 'client.email'])) {
$fields[$section][$field] = trans('texts.contact_' . $fieldName);
} else { } else {
$fields[$section][$field] = $labels[$fieldName]; $fields[$section][$field] = $labels[$fieldName];
} }
@ -208,7 +212,7 @@ trait PresentsInvoice
'website', 'website',
'phone', 'phone',
'blank', 'blank',
'adjustment', 'surcharge',
'tax_invoice', 'tax_invoice',
'tax_quote', 'tax_quote',
'statement', 'statement',
@ -216,6 +220,13 @@ trait PresentsInvoice
'your_statement', 'your_statement',
'statement_issued_to', 'statement_issued_to',
'statement_to', 'statement_to',
'credit_note',
'credit_date',
'credit_number',
'credit_issued_to',
'credit_to',
'your_credit',
'work_phone',
]; ];
foreach ($fields as $field) { foreach ($fields as $field) {

View File

@ -33,7 +33,7 @@ trait SendsEmails
{ {
if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) { if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) {
$field = "email_subject_{$entityType}"; $field = "email_subject_{$entityType}";
$value = $this->$field; $value = $this->account_email_settings->$field;
if ($value) { if ($value) {
return preg_replace("/\r\n|\r|\n/", ' ', $value); return preg_replace("/\r\n|\r|\n/", ' ', $value);
@ -84,7 +84,7 @@ trait SendsEmails
if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) { if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) {
$field = "email_template_{$entityType}"; $field = "email_template_{$entityType}";
$template = $this->$field; $template = $this->account_email_settings->$field;
} }
if (! $template) { if (! $template) {
@ -158,20 +158,27 @@ trait SendsEmails
public function setTemplateDefaults($type, $subject, $body) public function setTemplateDefaults($type, $subject, $body)
{ {
$settings = $this->account_email_settings;
if ($subject) { if ($subject) {
$this->{"email_subject_" . $type} = $subject; $settings->{"email_subject_" . $type} = $subject;
} }
if ($body) { if ($body) {
$this->{"email_template_" . $type} = $body; $settings->{"email_template_" . $type} = $body;
} }
$this->save(); $settings->save();
} }
public function getBccEmail() 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() 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 the user changes their email then they need to reconfirm it
if ($user->isEmailBeingChanged()) { if ($user->isEmailBeingChanged()) {
$user->confirmed = 0; $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 class AccountGatewayDatatable extends EntityDatatable
{ {
private static $accountGateways; private static $accountGateways;
private static $accountGatewaySettings;
public $entityType = ENTITY_ACCOUNT_GATEWAY; public $entityType = ENTITY_ACCOUNT_GATEWAY;
@ -19,7 +20,7 @@ class AccountGatewayDatatable extends EntityDatatable
{ {
return [ return [
[ [
'name', 'gateway',
function ($model) { function ($model) {
if ($model->deleted_at) { if ($model->deleted_at) {
return $model->name; return $model->name;
@ -62,26 +63,16 @@ class AccountGatewayDatatable extends EntityDatatable
[ [
'limit', 'limit',
function ($model) { function ($model) {
if ($model->gateway_id == GATEWAY_CUSTOM) { $gatewayTypes = $this->getGatewayTypes($model->id, $model->gateway_id);
$gatewayTypes = [GATEWAY_TYPE_CUSTOM];
} else {
$accountGateway = $this->getAccountGateway($model->id);
$paymentDriver = $accountGateway->paymentDriver();
$gatewayTypes = $paymentDriver->gatewayTypes();
$gatewayTypes = array_diff($gatewayTypes, [GATEWAY_TYPE_TOKEN]);
}
$html = ''; $html = '';
foreach ($gatewayTypes as $gatewayTypeId) { foreach ($gatewayTypes as $gatewayTypeId) {
$accountGatewaySettings = AccountGatewaySettings::scope()->where('account_gateway_settings.gateway_type_id', $accountGatewaySettings = $this->getAccountGatewaySetting($gatewayTypeId);
'=', $gatewayTypeId)->first(); $gatewayType = Utils::getFromCache($gatewayTypeId, 'gatewayTypes');
$gatewayType = GatewayType::find($gatewayTypeId);
if (count($gatewayTypes) > 1) { if (count($gatewayTypes) > 1) {
if ($html) { if ($html) {
$html .= '<br>'; $html .= '<br>';
} }
$html .= $gatewayType->name . ' &mdash; '; $html .= $gatewayType->name . ' &mdash; ';
} }
@ -103,6 +94,38 @@ class AccountGatewayDatatable extends EntityDatatable
return $html; 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) { foreach (Cache::get('gatewayTypes') as $gatewayType) {
$actions[] = [ $actions[] = [
trans('texts.set_limits', ['gateway_type' => $gatewayType->name]), trans('texts.set_limits_fees', ['gateway_type' => $gatewayType->name]),
function () use ($gatewayType) { function () use ($gatewayType) {
$accountGatewaySettings = AccountGatewaySettings::scope() return "javascript:showLimitsModal('{$gatewayType->name}', {$gatewayType->id})";
->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)";
}, },
function ($model) use ($gatewayType) { function ($model) use ($gatewayType) {
// Only show this action if the given gateway supports this gateway type // 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; return $gatewayType->id == GATEWAY_TYPE_CUSTOM;
} else { } else {
$accountGateway = $this->getAccountGateway($model->id); $accountGateway = $this->getAccountGateway($model->id);
$paymentDriver = $accountGateway->paymentDriver(); return $accountGateway->paymentDriver()->supportsGatewayType($gatewayType->id);
$gatewayTypes = $paymentDriver->gatewayTypes();
return in_array($gatewayType->id, $gatewayTypes);
} }
}, },
]; ];
@ -198,4 +212,30 @@ class AccountGatewayDatatable extends EntityDatatable
return static::$accountGateways[$id]; 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(); 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', 'client_created_at',
function ($model) { function ($model) {

View File

@ -41,10 +41,16 @@ class CreditDatatable extends EntityDatatable
'credit_date', 'credit_date',
function ($model) { function ($model) {
if (! Auth::user()->can('viewByOwner', [ENTITY_CREDIT, $model->user_id])) { 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', 'expense_date',
function ($model) { function ($model) {
if (! Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])) { 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', 'category',
function ($model) { 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', 'date',
function ($model) { 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', $entityType == ENTITY_INVOICE ? 'due_date' : 'valid_until',
function ($model) { 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})"; return "javascript:submitForm_{$entityType}('markPaid', {$model->public_id})";
}, },
function ($model) use ($entityType) { 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) 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); $label = Invoice::calcStatusLabel($model->invoice_status_name, $class, $this->entityType, $model->quote_invoice_id);
return "<h4><div class=\"label label-{$class}\">$label</div></h4>"; 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) { function ($model) {
if ($model->is_deleted) { if ($model->is_deleted) {
return Utils::dateToString($model->payment_date); return Utils::dateToString($model->payment_date);

View File

@ -5,6 +5,7 @@ namespace App\Ninja\Datatables;
use Auth; use Auth;
use URL; use URL;
use Utils; use Utils;
use App\Models\Invoice;
class RecurringInvoiceDatatable extends EntityDatatable class RecurringInvoiceDatatable extends EntityDatatable
{ {
@ -32,19 +33,19 @@ class RecurringInvoiceDatatable extends EntityDatatable
[ [
'start_date', 'start_date',
function ($model) { function ($model) {
return Utils::fromSqlDate($model->start_date); return Utils::fromSqlDate($model->start_date_sql);
}, },
], ],
[ [
'last_sent', 'last_sent',
function ($model) { function ($model) {
return Utils::fromSqlDate($model->last_sent_date); return Utils::fromSqlDate($model->last_sent_date_sql);
}, },
], ],
[ [
'end_date', 'end_date',
function ($model) { 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); 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() public function actions()
{ {
return [ return [

View File

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

View File

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

View File

@ -58,12 +58,7 @@ class UserMailer extends Mailer
$view = ($notificationType == 'approved' ? ENTITY_QUOTE : ENTITY_INVOICE) . "_{$notificationType}"; $view = ($notificationType == 'approved' ? ENTITY_QUOTE : ENTITY_INVOICE) . "_{$notificationType}";
$account = $user->account; $account = $user->account;
$client = $invoice->client; $client = $invoice->client;
$link = $invoice->present()->multiAccountLink;
if ($account->hasMultipleAccounts()) {
$link = url(sprintf('/account/%s?redirect_to=%s', $account->account_key, $invoice->present()->path));
} else {
$link = $invoice->present()->url;
}
$data = [ $data = [
'entityType' => $entityType, 'entityType' => $entityType,
@ -116,6 +111,26 @@ class UserMailer extends Mailer
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); $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) public function sendSecurityCode($user, $code)
{ {
if (! $user->email) { 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); 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 ($this->isGatewayType(GATEWAY_TYPE_TOKEN) || $gateway->is_offsite) {
if (Session::has('error')) { if (Session::has('error')) {
Session::reflash(); Session::reflash();
@ -161,6 +167,7 @@ class BasePaymentDriver
'invoiceNumber' => $this->invoice()->invoice_number, 'invoiceNumber' => $this->invoice()->invoice_number,
'client' => $this->client(), 'client' => $this->client(),
'contact' => $this->invitation->contact, 'contact' => $this->invitation->contact,
'invitation' => $this->invitation,
'gatewayType' => $this->gatewayType, 'gatewayType' => $this->gatewayType,
'currencyId' => $this->client()->getCurrencyId(), 'currencyId' => $this->client()->getCurrencyId(),
'currencyCode' => $this->client()->getCurrencyCode(), 'currencyCode' => $this->client()->getCurrencyCode(),
@ -262,6 +269,9 @@ class BasePaymentDriver
->firstOrFail(); ->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)) { if (! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) {
// The customer must have hacked the URL // The customer must have hacked the URL
Session::flash('error', trans('texts.limits_not_met')); 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 = trans('texts.payment_type_on_file', ['type' => $paymentMethod->payment_type->name]);
} }
$label .= $this->invoice()->present()->gatewayFee($paymentMethod->payment_type->gateway_type_id);
$links[] = [ $links[] = [
'url' => $url, 'url' => $url,
'label' => $label, 'label' => $label,
@ -886,6 +898,8 @@ class BasePaymentDriver
$label = trans("texts.{$gatewayTypeAlias}"); $label = trans("texts.{$gatewayTypeAlias}");
} }
$label .= $this->invoice()->present()->gatewayFee($gatewayTypeId);
$links[] = [ $links[] = [
'gatewayTypeId' => $gatewayTypeId, 'gatewayTypeId' => $gatewayTypeId,
'url' => $url, 'url' => $url,
@ -896,6 +910,11 @@ class BasePaymentDriver
return $links; return $links;
} }
public function supportsGatewayType($gatewayTypeId)
{
return in_array($gatewayTypeId, $this->gatewayTypes());
}
protected function meetsGatewayTypeLimits($gatewayTypeId) protected function meetsGatewayTypeLimits($gatewayTypeId)
{ {
if (! $gatewayTypeId) { if (! $gatewayTypeId) {
@ -925,17 +944,6 @@ class BasePaymentDriver
$account = $this->account(); $account = $this->account();
$url = URL::to("/payment/{$this->invitation->invitation_key}/{$gatewayTypeAlias}"); $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; return $url;
} }

View File

@ -5,6 +5,7 @@ namespace App\Ninja\PaymentDrivers;
use Braintree\Customer; use Braintree\Customer;
use Exception; use Exception;
use Session; use Session;
use App\Models\GatewayType;
class BraintreePaymentDriver extends BasePaymentDriver class BraintreePaymentDriver extends BasePaymentDriver
{ {
@ -62,6 +63,17 @@ class BraintreePaymentDriver extends BasePaymentDriver
return $customer instanceof Customer; 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) protected function paymentDetails($paymentMethod = false)
{ {
$data = parent::paymentDetails($paymentMethod); $data = parent::paymentDetails($paymentMethod);

View File

@ -17,6 +17,7 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
$data['ButtonSource'] = 'InvoiceNinja_SP'; $data['ButtonSource'] = 'InvoiceNinja_SP';
$data['solutionType'] = 'Sole'; // show 'Pay with credit card' option $data['solutionType'] = 'Sole'; // show 'Pay with credit card' option
$data['transactionId'] = $data['transactionId'] . '-' . time();
return $data; return $data;
} }
@ -27,4 +28,17 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
return $payment; 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['transaction_id'] = $transactionId;
} }
$data['applicationFee'] = (env('WEPAY_APP_FEE_MULTIPLIER') * $data['amount']) + env('WEPAY_APP_FEE_FIXED');
$data['feePayer'] = env('WEPAY_FEE_PAYER'); $data['feePayer'] = env('WEPAY_FEE_PAYER');
$data['callbackUri'] = $this->accountGateway->getWebhookUrl(); $data['callbackUri'] = $this->accountGateway->getWebhookUrl();
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod)) { if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod)) {
$data['paymentMethodType'] = 'payment_bank'; $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; $data['transaction_rbits'] = $this->invoice()->present()->rBits;

View File

@ -149,4 +149,28 @@ class AccountPresenter extends Presenter
return $options; 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')]; $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:submitBulkAction("markPaid")', 'label' => trans('texts.mark_paid')];
$actions[] = ['url' => 'javascript:onPaymentClick()', 'label' => trans('texts.enter_payment')]; $actions[] = ['url' => 'javascript:onPaymentClick()', 'label' => trans('texts.enter_payment')];
} }
@ -252,4 +252,45 @@ class InvoicePresenter extends EntityPresenter
return $actions; 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 run()
{ {
} }
public function results() public function results()
@ -66,7 +67,7 @@ class AbstractReport
if (strpos($field, 'date') !== false) { if (strpos($field, 'date') !== false) {
$class[] = 'group-date-' . (isset($this->options['group_dates_by']) ? $this->options['group_dates_by'] : 'monthyear'); $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'; $class[] = 'group-letter-100';
} elseif (in_array($field, ['amount', 'paid', 'balance'])) { } elseif (in_array($field, ['amount', 'paid', 'balance'])) {
$class[] = 'group-number-50'; $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; $account = Auth::user()->account;
$clients = Client::scope() $clients = Client::scope()
->orderBy('name')
->withArchived() ->withArchived()
->with('contacts') ->with('contacts')
->with(['invoices' => function ($query) { ->with(['invoices' => function ($query) {

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