mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 13:24:30 -04:00
Merge branch 'release-3.7.0'
This commit is contained in:
commit
6041c42270
@ -382,18 +382,14 @@ class CheckData extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$current = config('database.default');
|
||||
config(['database.default' => env('QUEUE_DATABASE')]);
|
||||
|
||||
$count = DB::table('failed_jobs')->count();
|
||||
$queueDB = config('queue.connections.database.connection');
|
||||
$count = DB::connection($queueDB)->table('failed_jobs')->count();
|
||||
|
||||
if ($count > 0) {
|
||||
$this->isValid = false;
|
||||
}
|
||||
|
||||
$this->logMessage($count . ' failed jobs');
|
||||
|
||||
config(['database.default' => $current]);
|
||||
}
|
||||
|
||||
private function checkBlankInvoiceHistory()
|
||||
|
@ -29,6 +29,11 @@ class UpdateKey extends Command
|
||||
{
|
||||
$this->info(date('Y-m-d h:i:s') . ' Running UpdateKey...');
|
||||
|
||||
if (! env('APP_KEY') || ! env('APP_CIPHER')) {
|
||||
$this->info(date('Y-m-d h:i:s') . ' Error: app key and cipher are not set');
|
||||
exit;
|
||||
}
|
||||
|
||||
// load the current values
|
||||
$gatewayConfigs = [];
|
||||
$bankUsernames = [];
|
||||
@ -41,9 +46,17 @@ class UpdateKey extends Command
|
||||
$bankUsernames[$bank->id] = $bank->getUsername();
|
||||
}
|
||||
|
||||
// set the new key and create a new encrypter
|
||||
Artisan::call('key:generate');
|
||||
$key = base64_decode(str_replace('base64:', '', config('app.key')));
|
||||
// check if we can write to the .env file
|
||||
$envPath = base_path() . '/.env';
|
||||
$envWriteable = file_exists($envPath) && @fopen($envPath, 'a');
|
||||
|
||||
if ($envWriteable) {
|
||||
Artisan::call('key:generate');
|
||||
$key = base64_decode(str_replace('base64:', '', config('app.key')));
|
||||
} else {
|
||||
$key = str_random(32);
|
||||
}
|
||||
|
||||
$crypt = new Encrypter($key, config('app.cipher'));
|
||||
|
||||
// update values using the new key/encrypter
|
||||
@ -59,7 +72,11 @@ class UpdateKey extends Command
|
||||
$bank->save();
|
||||
}
|
||||
|
||||
$this->info(date('Y-m-d h:i:s') . ' Successfully updated the application key');
|
||||
if ($envWriteable) {
|
||||
$this->info(date('Y-m-d h:i:s') . ' Successfully update the key');
|
||||
} else {
|
||||
$this->info(date('Y-m-d h:i:s') . ' Successfully update data, make sure to set the new app key: ' . $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,6 +38,7 @@ if (! defined('APP_NAME')) {
|
||||
define('ENTITY_EXPENSE_CATEGORY', 'expense_category');
|
||||
define('ENTITY_PROJECT', 'project');
|
||||
define('ENTITY_RECURRING_EXPENSE', 'recurring_expense');
|
||||
define('ENTITY_CUSTOMER', 'customer');
|
||||
|
||||
define('INVOICE_TYPE_STANDARD', 1);
|
||||
define('INVOICE_TYPE_QUOTE', 2);
|
||||
@ -169,6 +170,7 @@ if (! defined('APP_NAME')) {
|
||||
define('IMPORT_INVOICEABLE', 'Invoiceable');
|
||||
define('IMPORT_INVOICEPLANE', 'InvoicePlane');
|
||||
define('IMPORT_HARVEST', 'Harvest');
|
||||
define('IMPORT_STRIPE', 'Stripe');
|
||||
|
||||
define('MAX_NUM_CLIENTS', 100);
|
||||
define('MAX_NUM_CLIENTS_PRO', 20000);
|
||||
@ -271,11 +273,9 @@ if (! defined('APP_NAME')) {
|
||||
define('GATEWAY_SAGE_PAY_DIRECT', 20);
|
||||
define('GATEWAY_SAGE_PAY_SERVER', 21);
|
||||
define('GATEWAY_STRIPE', 23);
|
||||
define('GATEWAY_GOCARDLESS', 6);
|
||||
define('GATEWAY_TWO_CHECKOUT', 27);
|
||||
define('GATEWAY_BEANSTREAM', 29);
|
||||
define('GATEWAY_PSIGATE', 30);
|
||||
define('GATEWAY_MOOLAH', 31);
|
||||
define('GATEWAY_BITPAY', 42);
|
||||
define('GATEWAY_DWOLLA', 43);
|
||||
define('GATEWAY_CHECKOUT_COM', 47);
|
||||
@ -283,6 +283,7 @@ if (! defined('APP_NAME')) {
|
||||
define('GATEWAY_WEPAY', 60);
|
||||
define('GATEWAY_BRAINTREE', 61);
|
||||
define('GATEWAY_CUSTOM', 62);
|
||||
define('GATEWAY_GOCARDLESS', 64);
|
||||
|
||||
// The customer exists, but only as a local concept
|
||||
// The remote gateway doesn't understand the concept of customers
|
||||
@ -308,7 +309,7 @@ if (! defined('APP_NAME')) {
|
||||
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
|
||||
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
|
||||
define('NINJA_DATE', '2000-01-01');
|
||||
define('NINJA_VERSION', '3.6.1' . env('NINJA_VERSION_SUFFIX'));
|
||||
define('NINJA_VERSION', '3.7.0' . env('NINJA_VERSION_SUFFIX'));
|
||||
|
||||
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
|
||||
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
|
||||
@ -402,6 +403,8 @@ if (! defined('APP_NAME')) {
|
||||
define('PAYMENT_TYPE_MAESTRO', 21);
|
||||
define('PAYMENT_TYPE_SOLO', 22);
|
||||
define('PAYMENT_TYPE_SWITCH', 23);
|
||||
define('PAYMENT_TYPE_ALIPAY', 28);
|
||||
define('PAYMENT_TYPE_SOFORT', 29);
|
||||
|
||||
define('PAYMENT_METHOD_STATUS_NEW', 'new');
|
||||
define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed');
|
||||
@ -413,11 +416,17 @@ if (! defined('APP_NAME')) {
|
||||
define('GATEWAY_TYPE_BITCOIN', 4);
|
||||
define('GATEWAY_TYPE_DWOLLA', 5);
|
||||
define('GATEWAY_TYPE_CUSTOM', 6);
|
||||
define('GATEWAY_TYPE_ALIPAY', 7);
|
||||
define('GATEWAY_TYPE_SOFORT', 8);
|
||||
define('GATEWAY_TYPE_TOKEN', 'token');
|
||||
|
||||
define('REMINDER1', 'reminder1');
|
||||
define('REMINDER2', 'reminder2');
|
||||
define('REMINDER3', 'reminder3');
|
||||
define('TEMPLATE_INVOICE', 'invoice');
|
||||
define('TEMPLATE_QUOTE', 'quote');
|
||||
define('TEMPLATE_PARTIAL', 'partial');
|
||||
define('TEMPLATE_PAYMENT', 'payment');
|
||||
define('TEMPLATE_REMINDER1', 'reminder1');
|
||||
define('TEMPLATE_REMINDER2', 'reminder2');
|
||||
define('TEMPLATE_REMINDER3', 'reminder3');
|
||||
|
||||
define('RESET_FREQUENCY_DAILY', 1);
|
||||
define('RESET_FREQUENCY_WEEKLY', 2);
|
||||
@ -573,10 +582,20 @@ if (! defined('APP_NAME')) {
|
||||
// Fix for mPDF: https://github.com/kartik-v/yii2-mpdf/issues/9
|
||||
define('_MPDF_TTFONTDATAPATH', storage_path('framework/cache/'));
|
||||
|
||||
// TODO remove these translation functions
|
||||
function uctrans($text)
|
||||
function uctrans($text, $data = [])
|
||||
{
|
||||
return ucwords(trans($text));
|
||||
$locale = Session::get(SESSION_LOCALE);
|
||||
$text = trans($text, $data);
|
||||
|
||||
return $locale == 'en' ? ucwords($text) : $text;
|
||||
}
|
||||
|
||||
function utrans($text, $data = [])
|
||||
{
|
||||
$locale = Session::get(SESSION_LOCALE);
|
||||
$text = trans($text, $data);
|
||||
|
||||
return $locale == 'en' ? strtoupper($text) : $text;
|
||||
}
|
||||
|
||||
// optional trans: only return the string if it's translated
|
||||
|
@ -49,18 +49,19 @@ class Handler extends ExceptionHandler
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Crawler::isCrawler()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// don't show these errors in the logs
|
||||
if ($e instanceof NotFoundHttpException) {
|
||||
if (Crawler::isCrawler()) {
|
||||
return false;
|
||||
}
|
||||
// The logo can take a few seconds to get synced between servers
|
||||
// TODO: remove once we're using cloud storage for logos
|
||||
if (Utils::isNinja() && strpos(request()->url(), '/logo/') !== false) {
|
||||
return false;
|
||||
}
|
||||
// Log 404s to a separate file
|
||||
$errorStr = date('Y-m-d h:i:s') . ' ' . request()->url() . "\n" . json_encode(Utils::prepareErrorData('PHP')) . "\n\n";
|
||||
$errorStr = date('Y-m-d h:i:s') . ' ' . $e->getMessage() . ' URL:' . request()->url() . "\n" . json_encode(Utils::prepareErrorData('PHP')) . "\n\n";
|
||||
@file_put_contents(storage_path('logs/not-found.log'), $errorStr, FILE_APPEND);
|
||||
return false;
|
||||
} elseif ($e instanceof HttpResponseException) {
|
||||
@ -69,7 +70,7 @@ class Handler extends ExceptionHandler
|
||||
|
||||
if (! Utils::isTravis()) {
|
||||
Utils::logError(Utils::getErrorString($e));
|
||||
$stacktrace = date('Y-m-d h:i:s') . ' ' . $e->getTraceAsString() . "\n\n";
|
||||
$stacktrace = date('Y-m-d h:i:s') . ' ' . $e->getMessage() . ': ' . $e->getTraceAsString() . "\n\n";
|
||||
@file_put_contents(storage_path('logs/stacktrace.log'), $stacktrace, FILE_APPEND);
|
||||
return false;
|
||||
} else {
|
||||
|
@ -20,6 +20,7 @@ use App\Models\PaymentTerm;
|
||||
use App\Models\Product;
|
||||
use App\Models\TaxRate;
|
||||
use App\Models\User;
|
||||
use App\Models\AccountEmailSettings;
|
||||
use App\Ninja\Mailers\ContactMailer;
|
||||
use App\Ninja\Mailers\UserMailer;
|
||||
use App\Ninja\Repositories\AccountRepository;
|
||||
@ -653,7 +654,7 @@ class AccountController extends BaseController
|
||||
$data['account'] = $account;
|
||||
$data['templates'] = [];
|
||||
$data['defaultTemplates'] = [];
|
||||
foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) {
|
||||
foreach (AccountEmailSettings::$templates as $type) {
|
||||
$data['templates'][$type] = [
|
||||
'subject' => $account->getEmailSubject($type),
|
||||
'template' => $account->getEmailTemplate($type),
|
||||
@ -800,7 +801,7 @@ class AccountController extends BaseController
|
||||
if (Auth::user()->account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
|
||||
$account = Auth::user()->account;
|
||||
|
||||
foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) {
|
||||
foreach (AccountEmailSettings::$templates as $type) {
|
||||
$subjectField = "email_subject_{$type}";
|
||||
$subject = Input::get($subjectField, $account->getEmailSubject($type));
|
||||
$account->account_email_settings->$subjectField = ($subject == $account->getDefaultEmailSubject($type) ? null : $subject);
|
||||
@ -810,7 +811,7 @@ class AccountController extends BaseController
|
||||
$account->account_email_settings->$bodyField = ($body == $account->getDefaultEmailTemplate($type) ? null : $body);
|
||||
}
|
||||
|
||||
foreach ([REMINDER1, REMINDER2, REMINDER3] as $type) {
|
||||
foreach ([TEMPLATE_REMINDER1, TEMPLATE_REMINDER2, TEMPLATE_REMINDER3] as $type) {
|
||||
$enableField = "enable_{$type}";
|
||||
$account->$enableField = Input::get($enableField) ? true : false;
|
||||
$account->{"num_days_{$type}"} = Input::get("num_days_{$type}");
|
||||
@ -1364,7 +1365,12 @@ class AccountController extends BaseController
|
||||
|
||||
$user = Auth::user();
|
||||
$account = Auth::user()->account;
|
||||
|
||||
\Log::info("Canceled Account: {$account->name} - {$user->email}");
|
||||
$type = $account->hasMultipleAccounts() ? 'company' : 'account';
|
||||
$subject = trans("texts.deleted_{$type}");
|
||||
$message = trans("texts.deleted_{$type}_details", ['account' => $account->getDisplayName()]);
|
||||
$this->userMailer->sendMessage($user, $subject, $message);
|
||||
|
||||
$refunded = false;
|
||||
if (! $account->hasMultipleAccounts()) {
|
||||
|
@ -294,6 +294,11 @@ class AccountGatewayController extends BaseController
|
||||
$config->plaidPublicKey = $oldConfig->plaidPublicKey;
|
||||
}
|
||||
|
||||
if ($gatewayId == GATEWAY_STRIPE) {
|
||||
$config->enableAlipay = boolval(Input::get('enable_alipay'));
|
||||
$config->enableSofort = boolval(Input::get('enable_sofort'));
|
||||
}
|
||||
|
||||
if ($gatewayId == GATEWAY_STRIPE || $gatewayId == GATEWAY_WEPAY) {
|
||||
$config->enableAch = boolval(Input::get('enable_ach'));
|
||||
}
|
||||
|
@ -353,12 +353,30 @@ class AppController extends BaseController
|
||||
try {
|
||||
Artisan::call('ninja:check-data');
|
||||
Artisan::call('ninja:init-lookup', ['--validate' => true]);
|
||||
|
||||
// check error log is empty
|
||||
$errorLog = storage_path('logs/laravel-error.log');
|
||||
if (file_exists($errorLog)) {
|
||||
return 'Failure: error log exists';
|
||||
}
|
||||
|
||||
return RESULT_SUCCESS;
|
||||
} catch (Exception $exception) {
|
||||
return $exception->getMessage() ?: RESULT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
public function errors()
|
||||
{
|
||||
if (Utils::isNinjaProd()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
$errors = Utils::getErrors();
|
||||
|
||||
return view('errors.list', compact('errors'));
|
||||
}
|
||||
|
||||
public function stats()
|
||||
{
|
||||
if (! hash_equals(Input::get('password'), env('RESELLER_PASSWORD'))) {
|
||||
|
@ -117,6 +117,10 @@ class AuthController extends Controller
|
||||
*/
|
||||
public function getLoginWrapper()
|
||||
{
|
||||
if (auth()->check()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
if (! Utils::isNinja() && ! User::count()) {
|
||||
return redirect()->to('/setup');
|
||||
}
|
||||
|
@ -35,4 +35,18 @@ class PasswordController extends Controller
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the form to request a password reset link.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function getEmailWrapper()
|
||||
{
|
||||
if (auth()->check()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
return $this->getEmail();
|
||||
}
|
||||
}
|
||||
|
@ -69,10 +69,6 @@ class ClientPortalController extends BaseController
|
||||
|
||||
$account->loadLocalizationSettings($client);
|
||||
|
||||
if (! Input::has('phantomjs')) {
|
||||
$this->invoiceRepo->clearGatewayFee($invoice);
|
||||
}
|
||||
|
||||
if (! Input::has('phantomjs') && ! session('silent:' . $client->id) && ! Session::has($invitation->invitation_key)
|
||||
&& (! Auth::check() || Auth::user()->account_id != $invoice->account_id)) {
|
||||
if ($invoice->isType(INVOICE_TYPE_QUOTE)) {
|
||||
@ -130,6 +126,10 @@ class ClientPortalController extends BaseController
|
||||
if ($wepayGateway = $account->getGatewayConfig(GATEWAY_WEPAY)) {
|
||||
$data['enableWePayACH'] = $wepayGateway->getAchEnabled();
|
||||
}
|
||||
if ($stripeGateway = $account->getGatewayConfig(GATEWAY_STRIPE)) {
|
||||
//$data['enableStripeSources'] = $stripeGateway->getAlipayEnabled();
|
||||
$data['enableStripeSources'] = true;
|
||||
}
|
||||
|
||||
$showApprove = $invoice->quote_invoice_id ? false : true;
|
||||
if ($invoice->due_date) {
|
||||
|
@ -198,7 +198,7 @@ class ExportController extends BaseController
|
||||
if ($request->input('include') === 'all' || $request->input('invoices')) {
|
||||
$data['invoices'] = Invoice::scope()
|
||||
->invoiceType(INVOICE_TYPE_STANDARD)
|
||||
->with('user', 'client.contacts', 'invoice_status')
|
||||
->with('user', 'client.contacts', 'invoice_status', 'invoice_items')
|
||||
->withArchived()
|
||||
->where('is_recurring', '=', false)
|
||||
->get();
|
||||
@ -207,7 +207,7 @@ class ExportController extends BaseController
|
||||
if ($request->input('include') === 'all' || $request->input('quotes')) {
|
||||
$data['quotes'] = Invoice::scope()
|
||||
->invoiceType(INVOICE_TYPE_QUOTE)
|
||||
->with('user', 'client.contacts', 'invoice_status')
|
||||
->with('user', 'client.contacts', 'invoice_status', 'invoice_items')
|
||||
->withArchived()
|
||||
->where('is_recurring', '=', false)
|
||||
->get();
|
||||
@ -216,7 +216,7 @@ class ExportController extends BaseController
|
||||
if ($request->input('include') === 'all' || $request->input('recurring')) {
|
||||
$data['recurringInvoices'] = Invoice::scope()
|
||||
->invoiceType(INVOICE_TYPE_STANDARD)
|
||||
->with('user', 'client.contacts', 'invoice_status', 'frequency')
|
||||
->with('user', 'client.contacts', 'invoice_status', 'frequency', 'invoice_items')
|
||||
->withArchived()
|
||||
->where('is_recurring', '=', true)
|
||||
->get();
|
||||
|
@ -131,12 +131,18 @@ class HomeController extends BaseController
|
||||
*/
|
||||
public function contactUs()
|
||||
{
|
||||
Mail::raw(request()->contact_us_message, function ($message) {
|
||||
$message = request()->contact_us_message;
|
||||
|
||||
if (request()->include_errors) {
|
||||
$message .= "\n\n" . join("\n", Utils::getErrors());
|
||||
}
|
||||
|
||||
Mail::raw($message, function ($message) {
|
||||
$subject = 'Customer Message: ';
|
||||
if (Utils::isNinja()) {
|
||||
$subject .= config('database.default');
|
||||
if (Utils::isNinjaProd()) {
|
||||
$subject .= str_replace('db-', '', config('database.default'));
|
||||
} else {
|
||||
$subject .= 'v' . NINJA_VERSION;
|
||||
$subject .= 'Self-Host';
|
||||
}
|
||||
$message->to(env('CONTACT_EMAIL', 'contact@invoiceninja.com'))
|
||||
->from(CONTACT_EMAIL, Auth::user()->present()->fullName)
|
||||
|
@ -206,7 +206,8 @@ class InvoiceApiController extends BaseAPIController
|
||||
if ($invoice->is_recurring && $recurringInvoice = $this->invoiceRepo->createRecurringInvoice($invoice)) {
|
||||
$invoice = $recurringInvoice;
|
||||
}
|
||||
app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice);
|
||||
$reminder = isset($data['email_type']) ? $data['email_type'] : false;
|
||||
app('App\Ninja\Mailers\ContactMailer')->sendInvoice($invoice, $reminder);
|
||||
//$this->dispatch(new SendInvoiceEmail($invoice));
|
||||
}
|
||||
}
|
||||
@ -423,6 +424,11 @@ class InvoiceApiController extends BaseAPIController
|
||||
public function download(InvoiceRequest $request)
|
||||
{
|
||||
$invoice = $request->entity();
|
||||
|
||||
if ($invoice->is_deleted) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$pdfString = $invoice->getPDFString();
|
||||
|
||||
if ($pdfString) {
|
||||
|
@ -98,8 +98,11 @@ class InvoiceController extends BaseController
|
||||
$clients = Client::scope()->withTrashed()->with('contacts', 'country');
|
||||
|
||||
if ($clone) {
|
||||
$entityType = $clone == INVOICE_TYPE_STANDARD ? ENTITY_INVOICE : ENTITY_QUOTE;
|
||||
$invoice->id = $invoice->public_id = null;
|
||||
$invoice->is_public = false;
|
||||
$invoice->is_recurring = $invoice->is_recurring && $clone == INVOICE_TYPE_STANDARD;
|
||||
$invoice->invoice_type_id = $clone;
|
||||
$invoice->invoice_number = $account->getNextNumber($invoice);
|
||||
$invoice->due_date = null;
|
||||
$invoice->balance = $invoice->amount;
|
||||
@ -371,8 +374,10 @@ class InvoiceController extends BaseController
|
||||
$message = trans("texts.updated_{$entityType}");
|
||||
Session::flash('message', $message);
|
||||
|
||||
if ($action == 'clone') {
|
||||
return url(sprintf('%ss/%s/clone', $entityType, $invoice->public_id));
|
||||
if ($action == 'clone_invoice') {
|
||||
return url(sprintf('invoices/%s/clone', $invoice->public_id));
|
||||
} else if ($action == 'clone_quote') {
|
||||
return url(sprintf('quotes/%s/clone', $invoice->public_id));
|
||||
} elseif ($action == 'convert') {
|
||||
return $this->convertQuote($request, $invoice->public_id);
|
||||
} elseif ($action == 'email') {
|
||||
@ -506,7 +511,12 @@ class InvoiceController extends BaseController
|
||||
|
||||
public function cloneInvoice(InvoiceRequest $request, $publicId)
|
||||
{
|
||||
return self::edit($request, $publicId, true);
|
||||
return self::edit($request, $publicId, INVOICE_TYPE_STANDARD);
|
||||
}
|
||||
|
||||
public function cloneQuote(InvoiceRequest $request, $publicId)
|
||||
{
|
||||
return self::edit($request, $publicId, INVOICE_TYPE_QUOTE);
|
||||
}
|
||||
|
||||
public function invoiceHistory(InvoiceRequest $request)
|
||||
|
@ -149,6 +149,10 @@ class OnlinePaymentController extends BaseController
|
||||
*/
|
||||
public function offsitePayment($invitationKey = false, $gatewayTypeAlias = false)
|
||||
{
|
||||
if (Crawler::isCrawler()) {
|
||||
return redirect()->to(NINJA_WEB_URL, 301);
|
||||
}
|
||||
|
||||
$invitationKey = $invitationKey ?: Session::get('invitation_key');
|
||||
$invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway')
|
||||
->where('invitation_key', '=', $invitationKey)->firstOrFail();
|
||||
@ -194,6 +198,18 @@ class OnlinePaymentController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function completeSource($invitationKey, $gatewayType)
|
||||
{
|
||||
if (! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
|
||||
return response()->view('error', [
|
||||
'error' => trans('texts.invoice_not_found'),
|
||||
'hideHeader' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->to('view/' . $invitation->invitation_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $paymentDriver
|
||||
* @param $exception
|
||||
@ -283,7 +299,9 @@ class OnlinePaymentController extends BaseController
|
||||
|
||||
return response()->json(['message' => $result]);
|
||||
} catch (Exception $exception) {
|
||||
//Utils::logError($exception->getMessage(), 'PHP');
|
||||
if (! Utils::isNinjaProd()) {
|
||||
Utils::logError($exception->getMessage(), 'HOOK');
|
||||
}
|
||||
|
||||
return response()->json(['message' => $exception->getMessage()], 500);
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ class PaymentController extends BaseController
|
||||
{
|
||||
$invoices = Invoice::scope()
|
||||
->invoices()
|
||||
->where('invoices.balance', '!=', 0)
|
||||
->where('invoices.invoice_status_id', '!=', INVOICE_STATUS_PAID)
|
||||
->with('client', 'invoice_status')
|
||||
->orderBy('invoice_number')->get();
|
||||
|
||||
|
@ -65,6 +65,21 @@ class UserController extends BaseController
|
||||
return Redirect::to('/dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @param mixed $publicId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function show($publicId)
|
||||
{
|
||||
Session::reflash();
|
||||
|
||||
return redirect("users/$publicId/edit");
|
||||
}
|
||||
|
||||
public function edit($publicId)
|
||||
{
|
||||
$user = User::where('account_id', '=', Auth::user()->account_id)
|
||||
|
@ -72,11 +72,38 @@ class StartupCheck
|
||||
}
|
||||
}
|
||||
|
||||
// Check the application is up to date and for any news feed messages
|
||||
if (Auth::check()) {
|
||||
$company = Auth::user()->account->company;
|
||||
$count = Session::get(SESSION_COUNTER, 0);
|
||||
Session::put(SESSION_COUNTER, ++$count);
|
||||
|
||||
if (Utils::isNinja()) {
|
||||
if ($coupon = request()->coupon) {
|
||||
if ($code = config('ninja.coupon_50_off')) {
|
||||
if (hash_equals($coupon, $code)) {
|
||||
$company->applyDiscount(.5);
|
||||
$company->save();
|
||||
Session::flash('message', trans('texts.applied_discount', ['discount' => 50]));
|
||||
}
|
||||
}
|
||||
if ($code = config('ninja.coupon_75_off')) {
|
||||
if (hash_equals($coupon, $code)) {
|
||||
$company->applyDiscount(.75);
|
||||
$company->save();
|
||||
Session::flash('message', trans('texts.applied_discount', ['discount' => 75]));
|
||||
}
|
||||
}
|
||||
if ($code = config('ninja.coupon_free_year')) {
|
||||
if (hash_equals($coupon, $code)) {
|
||||
$company->applyFreeYear();
|
||||
$company->save();
|
||||
Session::flash('message', trans('texts.applied_free_year'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check the application is up to date and for any news feed messages
|
||||
if (isset($_SERVER['REQUEST_URI']) && ! Utils::startsWith($_SERVER['REQUEST_URI'], '/news_feed') && ! Session::has('news_feed_id')) {
|
||||
$data = false;
|
||||
if (Utils::isNinja()) {
|
||||
@ -144,7 +171,6 @@ class StartupCheck
|
||||
if ($data == RESULT_FAILURE) {
|
||||
Session::flash('error', trans('texts.invalid_white_label_license'));
|
||||
} elseif ($data) {
|
||||
$company = Auth::user()->account->company;
|
||||
$company->plan_term = PLAN_TERM_YEARLY;
|
||||
$company->plan_paid = $data;
|
||||
$date = max(date_create($data), date_create($company->plan_expires));
|
||||
|
33
app/Http/Requests/CreateCustomerRequest.php
Normal file
33
app/Http/Requests/CreateCustomerRequest.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class CreateCustomerRequest extends CustomerRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return $this->user()->can('create', ENTITY_CUSTOMER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
$rules = [
|
||||
'token' => 'required',
|
||||
'client_id' => 'required',
|
||||
'contact_id' => 'required',
|
||||
'payment_method.source_reference' => 'required',
|
||||
];
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ class CreatePaymentAPIRequest extends PaymentRequest
|
||||
if (! $this->invoice_id || ! $this->amount) {
|
||||
return [
|
||||
'invoice_id' => 'required|numeric|min:1',
|
||||
'amount' => 'required|numeric|not_in:0',
|
||||
'amount' => 'required|numeric',
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ class CreatePaymentRequest extends PaymentRequest
|
||||
$rules = [
|
||||
'client' => 'required', // TODO: change to client_id once views are updated
|
||||
'invoice' => 'required', // TODO: change to invoice_id once views are updated
|
||||
'amount' => 'required|numeric|not_in:0',
|
||||
'amount' => 'required|numeric',
|
||||
'payment_date' => 'required',
|
||||
];
|
||||
|
||||
|
8
app/Http/Requests/CustomerRequest.php
Normal file
8
app/Http/Requests/CustomerRequest.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class CustomerRequest extends EntityRequest
|
||||
{
|
||||
protected $entityType = ENTITY_CUSTOMER;
|
||||
}
|
@ -18,6 +18,8 @@ class EntityRequest extends Request
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
$class = EntityModel::getClassName($this->entityType);
|
||||
|
||||
// The entity id can appear as invoices, invoice_id, public_id or id
|
||||
$publicId = false;
|
||||
$field = $this->entityType . '_id';
|
||||
@ -37,8 +39,6 @@ class EntityRequest extends Request
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = EntityModel::getClassName($this->entityType);
|
||||
|
||||
if (method_exists($class, 'trashed')) {
|
||||
$this->entity = $class::scope($publicId)->withTrashed()->firstOrFail();
|
||||
} else {
|
||||
|
@ -33,6 +33,7 @@ Route::group(['middleware' => ['lookup:contact', 'auth:client']], function () {
|
||||
Route::get('approve/{invitation_key}', 'QuoteController@approve');
|
||||
Route::get('payment/{invitation_key}/{gateway_type?}/{source_id?}', 'OnlinePaymentController@showPayment');
|
||||
Route::post('payment/{invitation_key}', 'OnlinePaymentController@doPayment');
|
||||
Route::get('complete_source/{invitation_key}/{gateway_type}', 'OnlinePaymentController@completeSource');
|
||||
Route::match(['GET', 'POST'], 'complete/{invitation_key?}/{gateway_type?}', 'OnlinePaymentController@offsitePayment');
|
||||
Route::get('bank/{routing_number}', 'OnlinePaymentController@getBankInfo');
|
||||
Route::get('client/payment_methods', 'ClientPortalController@paymentMethods');
|
||||
@ -87,7 +88,7 @@ Route::get('/signup', ['as' => 'signup', 'uses' => 'Auth\AuthController@getRegis
|
||||
Route::post('/signup', ['as' => 'signup', 'uses' => 'Auth\AuthController@postRegister']);
|
||||
Route::get('/login', ['as' => 'login', 'uses' => 'Auth\AuthController@getLoginWrapper']);
|
||||
Route::get('/logout', ['as' => 'logout', 'uses' => 'Auth\AuthController@getLogoutWrapper']);
|
||||
Route::get('/recover_password', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@getEmail']);
|
||||
Route::get('/recover_password', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@getEmailWrapper']);
|
||||
Route::get('/password/reset/{token}', ['as' => 'forgot', 'uses' => 'Auth\PasswordController@getReset']);
|
||||
Route::get('/auth/{provider}', 'Auth\AuthController@authLogin');
|
||||
|
||||
@ -156,6 +157,7 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
|
||||
Route::post('projects', 'ProjectController@store');
|
||||
Route::put('projects/{projects}', 'ProjectController@update');
|
||||
Route::get('projects/{projects}/edit', 'ProjectController@edit');
|
||||
Route::get('projects/{projects}', 'ProjectController@edit');
|
||||
Route::post('projects/bulk', 'ProjectController@bulk');
|
||||
|
||||
Route::get('api/recurring_invoices/{client_id?}', 'InvoiceController@getRecurringDatatable');
|
||||
@ -190,7 +192,7 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
|
||||
Route::delete('documents/{documents}', 'DocumentController@delete');
|
||||
|
||||
Route::get('quotes/create/{client_id?}', 'QuoteController@create');
|
||||
Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice');
|
||||
Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneQuote');
|
||||
Route::get('quotes/{invoices}/edit', 'InvoiceController@edit');
|
||||
Route::put('quotes/{invoices}', 'InvoiceController@update');
|
||||
Route::get('quotes/{invoices}', 'InvoiceController@edit');
|
||||
@ -260,6 +262,7 @@ Route::group([
|
||||
Route::get('/account/{account_key}', 'UserController@viewAccountByKey');
|
||||
Route::get('/unlink_account/{user_account_id}/{user_id}', 'UserController@unlinkAccount');
|
||||
Route::get('/manage_companies', 'UserController@manageCompanies');
|
||||
Route::get('/errors', 'AppController@errors');
|
||||
|
||||
Route::get('api/tokens', 'TokenController@getDatatable');
|
||||
Route::resource('tokens', 'TokenController');
|
||||
|
@ -1,879 +0,0 @@
|
||||
<?php
|
||||
|
||||
class parseCSV
|
||||
{
|
||||
/*
|
||||
|
||||
Class: parseCSV v0.3.2
|
||||
http://code.google.com/p/parsecsv-for-php/
|
||||
|
||||
|
||||
Fully conforms to the specifications lined out on wikipedia:
|
||||
- http://en.wikipedia.org/wiki/Comma-separated_values
|
||||
|
||||
Based on the concept of Ming Hong Ng's CsvFileParser class:
|
||||
- http://minghong.blogspot.com/2006/07/csv-parser-for-php.html
|
||||
|
||||
|
||||
|
||||
Copyright (c) 2007 Jim Myhrberg (jim@zydev.info).
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
|
||||
Code Examples
|
||||
----------------
|
||||
# general usage
|
||||
$csv = new parseCSV('data.csv');
|
||||
print_r($csv->data);
|
||||
----------------
|
||||
# tab delimited, and encoding conversion
|
||||
$csv = new parseCSV();
|
||||
$csv->encoding('UTF-16', 'UTF-8');
|
||||
$csv->delimiter = "\t";
|
||||
$csv->parse('data.tsv');
|
||||
print_r($csv->data);
|
||||
----------------
|
||||
# auto-detect delimiter character
|
||||
$csv = new parseCSV();
|
||||
$csv->auto('data.csv');
|
||||
print_r($csv->data);
|
||||
----------------
|
||||
# modify data in a csv file
|
||||
$csv = new parseCSV();
|
||||
$csv->sort_by = 'id';
|
||||
$csv->parse('data.csv');
|
||||
# "4" is the value of the "id" column of the CSV row
|
||||
$csv->data[4] = array('firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@doe.com');
|
||||
$csv->save();
|
||||
----------------
|
||||
# add row/entry to end of CSV file
|
||||
# - only recommended when you know the extact sctructure of the file
|
||||
$csv = new parseCSV();
|
||||
$csv->save('data.csv', array('1986', 'Home', 'Nowhere', ''), true);
|
||||
----------------
|
||||
# convert 2D array to csv data and send headers
|
||||
# to browser to treat output as a file and download it
|
||||
$csv = new parseCSV();
|
||||
$csv->output (true, 'movies.csv', $array);
|
||||
----------------
|
||||
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration
|
||||
* - set these options with $object->var_name = 'value';.
|
||||
*/
|
||||
|
||||
// use first line/entry as field names
|
||||
public $heading = true;
|
||||
|
||||
// override field names
|
||||
public $fields = [];
|
||||
|
||||
// sort entries by this field
|
||||
public $sort_by = null;
|
||||
public $sort_reverse = false;
|
||||
|
||||
// delimiter (comma) and enclosure (double quote)
|
||||
public $delimiter = ',';
|
||||
public $enclosure = '"';
|
||||
|
||||
// basic SQL-like conditions for row matching
|
||||
public $conditions = null;
|
||||
|
||||
// number of rows to ignore from beginning of data
|
||||
public $offset = null;
|
||||
|
||||
// limits the number of returned rows to specified amount
|
||||
public $limit = null;
|
||||
|
||||
// number of rows to analyze when attempting to auto-detect delimiter
|
||||
public $auto_depth = 15;
|
||||
|
||||
// characters to ignore when attempting to auto-detect delimiter
|
||||
public $auto_non_chars = "a-zA-Z0-9\n\r";
|
||||
|
||||
// preferred delimiter characters, only used when all filtering method
|
||||
// returns multiple possible delimiters (happens very rarely)
|
||||
public $auto_preferred = ",;\t.:|";
|
||||
|
||||
// character encoding options
|
||||
public $convert_encoding = false;
|
||||
public $input_encoding = 'ISO-8859-1';
|
||||
public $output_encoding = 'ISO-8859-1';
|
||||
|
||||
// used by unparse(), save(), and output() functions
|
||||
public $linefeed = "\r\n";
|
||||
|
||||
// only used by output() function
|
||||
public $output_delimiter = ',';
|
||||
public $output_filename = 'data.csv';
|
||||
|
||||
/**
|
||||
* Internal variables.
|
||||
*/
|
||||
|
||||
// current file
|
||||
public $file;
|
||||
|
||||
// loaded file contents
|
||||
public $file_data;
|
||||
|
||||
// array of field values in data parsed
|
||||
public $titles = [];
|
||||
|
||||
// two dimentional array of CSV data
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param input CSV file or string
|
||||
* @param null|mixed $input
|
||||
* @param null|mixed $offset
|
||||
* @param null|mixed $limit
|
||||
* @param null|mixed $conditions
|
||||
*
|
||||
* @return nothing
|
||||
*/
|
||||
public function __construct($input = null, $offset = null, $limit = null, $conditions = null)
|
||||
{
|
||||
if ($offset !== null) {
|
||||
$this->offset = $offset;
|
||||
}
|
||||
if ($limit !== null) {
|
||||
$this->limit = $limit;
|
||||
}
|
||||
if (count($conditions) > 0) {
|
||||
$this->conditions = $conditions;
|
||||
}
|
||||
if (! empty($input)) {
|
||||
$this->parse($input);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ----- [ Main Functions ] ---------------------
|
||||
// ==============================================
|
||||
|
||||
/**
|
||||
* Parse CSV file or string.
|
||||
*
|
||||
* @param input CSV file or string
|
||||
* @param null|mixed $input
|
||||
* @param null|mixed $offset
|
||||
* @param null|mixed $limit
|
||||
* @param null|mixed $conditions
|
||||
*
|
||||
* @return nothing
|
||||
*/
|
||||
public function parse($input = null, $offset = null, $limit = null, $conditions = null)
|
||||
{
|
||||
if (! empty($input)) {
|
||||
if ($offset !== null) {
|
||||
$this->offset = $offset;
|
||||
}
|
||||
if ($limit !== null) {
|
||||
$this->limit = $limit;
|
||||
}
|
||||
if (count($conditions) > 0) {
|
||||
$this->conditions = $conditions;
|
||||
}
|
||||
if (is_readable($input)) {
|
||||
$this->data = $this->parse_file($input);
|
||||
} else {
|
||||
$this->file_data = &$input;
|
||||
$this->data = $this->parse_string();
|
||||
}
|
||||
if ($this->data === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save changes, or new file and/or data.
|
||||
*
|
||||
* @param file file to save to
|
||||
* @param data 2D array with data
|
||||
* @param append append current data to end of target CSV if exists
|
||||
* @param fields field names
|
||||
* @param null|mixed $file
|
||||
* @param mixed $data
|
||||
* @param mixed $append
|
||||
* @param mixed $fields
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public function save($file = null, $data = [], $append = false, $fields = [])
|
||||
{
|
||||
if (empty($file)) {
|
||||
$file = &$this->file;
|
||||
}
|
||||
$mode = ($append) ? 'at' : 'wt';
|
||||
$is_php = (preg_match('/\.php$/i', $file)) ? true : false;
|
||||
|
||||
return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSV based string for output.
|
||||
*
|
||||
* @param output if true, prints headers and strings to browser
|
||||
* @param filename filename sent to browser in headers if output is true
|
||||
* @param data 2D array with data
|
||||
* @param fields field names
|
||||
* @param delimiter delimiter used to separate data
|
||||
* @param mixed $output
|
||||
* @param null|mixed $filename
|
||||
* @param mixed $data
|
||||
* @param mixed $fields
|
||||
* @param null|mixed $delimiter
|
||||
*
|
||||
* @return CSV data using delimiter of choice, or default
|
||||
*/
|
||||
public function output($output = true, $filename = null, $data = [], $fields = [], $delimiter = null)
|
||||
{
|
||||
if (empty($filename)) {
|
||||
$filename = $this->output_filename;
|
||||
}
|
||||
if ($delimiter === null) {
|
||||
$delimiter = $this->output_delimiter;
|
||||
}
|
||||
$data = $this->unparse($data, $fields, null, null, $delimiter);
|
||||
if ($output) {
|
||||
header('Content-type: application/csv');
|
||||
header('Content-Disposition: inline; filename="'.$filename.'"');
|
||||
echo $data;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert character encoding.
|
||||
*
|
||||
* @param input input character encoding, uses default if left blank
|
||||
* @param output output character encoding, uses default if left blank
|
||||
* @param null|mixed $input
|
||||
* @param null|mixed $output
|
||||
*
|
||||
* @return nothing
|
||||
*/
|
||||
public function encoding($input = null, $output = null)
|
||||
{
|
||||
$this->convert_encoding = true;
|
||||
if ($input !== null) {
|
||||
$this->input_encoding = $input;
|
||||
}
|
||||
if ($output !== null) {
|
||||
$this->output_encoding = $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Detect Delimiter: Find delimiter by analyzing a specific number of
|
||||
* rows to determine most probable delimiter character.
|
||||
*
|
||||
* @param file local CSV file
|
||||
* @param parse true/false parse file directly
|
||||
* @param search_depth number of rows to analyze
|
||||
* @param preferred preferred delimiter characters
|
||||
* @param enclosure enclosure character, default is double quote (").
|
||||
* @param null|mixed $file
|
||||
* @param mixed $parse
|
||||
* @param null|mixed $search_depth
|
||||
* @param null|mixed $preferred
|
||||
* @param null|mixed $enclosure
|
||||
*
|
||||
* @return delimiter character
|
||||
*/
|
||||
public function auto($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null)
|
||||
{
|
||||
if ($file === null) {
|
||||
$file = $this->file;
|
||||
}
|
||||
if (empty($search_depth)) {
|
||||
$search_depth = $this->auto_depth;
|
||||
}
|
||||
if ($enclosure === null) {
|
||||
$enclosure = $this->enclosure;
|
||||
}
|
||||
|
||||
if ($preferred === null) {
|
||||
$preferred = $this->auto_preferred;
|
||||
}
|
||||
|
||||
if (empty($this->file_data)) {
|
||||
if ($this->_check_data($file)) {
|
||||
$data = &$this->file_data;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$data = &$this->file_data;
|
||||
}
|
||||
|
||||
$chars = [];
|
||||
$strlen = strlen($data);
|
||||
$enclosed = false;
|
||||
$n = 1;
|
||||
$to_end = true;
|
||||
|
||||
// walk specific depth finding posssible delimiter characters
|
||||
for ($i = 0; $i < $strlen; $i++) {
|
||||
$ch = $data{$i};
|
||||
$nch = (isset($data{$i + 1})) ? $data{$i + 1} : false;
|
||||
$pch = (isset($data{$i - 1})) ? $data{$i - 1} : false;
|
||||
|
||||
// open and closing quotes
|
||||
if ($ch == $enclosure && (! $enclosed || $nch != $enclosure)) {
|
||||
$enclosed = ($enclosed) ? false : true;
|
||||
|
||||
// inline quotes
|
||||
} elseif ($ch == $enclosure && $enclosed) {
|
||||
$i++;
|
||||
|
||||
// end of row
|
||||
} elseif (($ch == "\n" && $pch != "\r" || $ch == "\r") && ! $enclosed) {
|
||||
if ($n >= $search_depth) {
|
||||
$strlen = 0;
|
||||
$to_end = false;
|
||||
} else {
|
||||
$n++;
|
||||
}
|
||||
|
||||
// count character
|
||||
} elseif (! $enclosed) {
|
||||
if (! preg_match('/['.preg_quote($this->auto_non_chars, '/').']/i', $ch)) {
|
||||
if (! isset($chars[$ch][$n])) {
|
||||
$chars[$ch][$n] = 1;
|
||||
} else {
|
||||
$chars[$ch][$n]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filtering
|
||||
$depth = ($to_end) ? $n - 1 : $n;
|
||||
$filtered = [];
|
||||
foreach ($chars as $char => $value) {
|
||||
if ($match = $this->_check_count($char, $value, $depth, $preferred)) {
|
||||
$filtered[$match] = $char;
|
||||
}
|
||||
}
|
||||
|
||||
// capture most probable delimiter
|
||||
ksort($filtered);
|
||||
$delimiter = reset($filtered);
|
||||
$this->delimiter = $delimiter;
|
||||
|
||||
// parse data
|
||||
if ($parse) {
|
||||
$this->data = $this->parse_string();
|
||||
}
|
||||
|
||||
return $delimiter;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ----- [ Core Functions ] ---------------------
|
||||
// ==============================================
|
||||
|
||||
/**
|
||||
* Read file to string and call parse_string().
|
||||
*
|
||||
* @param file local CSV file
|
||||
* @param null|mixed $file
|
||||
*
|
||||
* @return 2D array with CSV data, or false on failure
|
||||
*/
|
||||
public function parse_file($file = null)
|
||||
{
|
||||
if ($file === null) {
|
||||
$file = $this->file;
|
||||
}
|
||||
if (empty($this->file_data)) {
|
||||
$this->load_data($file);
|
||||
}
|
||||
|
||||
return (! empty($this->file_data)) ? $this->parse_string() : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CSV strings to arrays.
|
||||
*
|
||||
* @param data CSV string
|
||||
* @param null|mixed $data
|
||||
*
|
||||
* @return 2D array with CSV data, or false on failure
|
||||
*/
|
||||
public function parse_string($data = null)
|
||||
{
|
||||
if (empty($data)) {
|
||||
if ($this->_check_data()) {
|
||||
$data = &$this->file_data;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$row = [];
|
||||
$row_count = 0;
|
||||
$current = '';
|
||||
$head = (! empty($this->fields)) ? $this->fields : [];
|
||||
$col = 0;
|
||||
$enclosed = false;
|
||||
$was_enclosed = false;
|
||||
$strlen = strlen($data);
|
||||
|
||||
// walk through each character
|
||||
for ($i = 0; $i < $strlen; $i++) {
|
||||
$ch = $data{$i};
|
||||
$nch = (isset($data{$i + 1})) ? $data{$i + 1} : false;
|
||||
$pch = (isset($data{$i - 1})) ? $data{$i - 1} : false;
|
||||
|
||||
// open and closing quotes
|
||||
if ($ch == $this->enclosure && (! $enclosed || $nch != $this->enclosure)) {
|
||||
$enclosed = ($enclosed) ? false : true;
|
||||
if ($enclosed) {
|
||||
$was_enclosed = true;
|
||||
}
|
||||
|
||||
// inline quotes
|
||||
} elseif ($ch == $this->enclosure && $enclosed) {
|
||||
$current .= $ch;
|
||||
$i++;
|
||||
|
||||
// end of field/row
|
||||
} elseif (($ch == $this->delimiter || ($ch == "\n" && $pch != "\r") || $ch == "\r") && ! $enclosed) {
|
||||
if (! $was_enclosed) {
|
||||
$current = trim($current);
|
||||
}
|
||||
$key = (! empty($head[$col])) ? $head[$col] : $col;
|
||||
$row[$key] = $current;
|
||||
$current = '';
|
||||
$col++;
|
||||
|
||||
// end of row
|
||||
if ($ch == "\n" || $ch == "\r") {
|
||||
if ($this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions)) {
|
||||
if ($this->heading && empty($head)) {
|
||||
$head = $row;
|
||||
} elseif (empty($this->fields) || (! empty($this->fields) && (($this->heading && $row_count > 0) || ! $this->heading))) {
|
||||
if (! empty($this->sort_by) && ! empty($row[$this->sort_by])) {
|
||||
if (isset($rows[$row[$this->sort_by]])) {
|
||||
$rows[$row[$this->sort_by].'_0'] = &$rows[$row[$this->sort_by]];
|
||||
unset($rows[$row[$this->sort_by]]);
|
||||
for ($sn = 1; isset($rows[$row[$this->sort_by].'_'.$sn]); $sn++) {
|
||||
}
|
||||
$rows[$row[$this->sort_by].'_'.$sn] = $row;
|
||||
} else {
|
||||
$rows[$row[$this->sort_by]] = $row;
|
||||
}
|
||||
} else {
|
||||
$rows[] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
$row = [];
|
||||
$col = 0;
|
||||
$row_count++;
|
||||
if ($this->sort_by === null && $this->limit !== null && count($rows) == $this->limit) {
|
||||
$i = $strlen;
|
||||
}
|
||||
}
|
||||
|
||||
// append character to current field
|
||||
} else {
|
||||
$current .= $ch;
|
||||
}
|
||||
}
|
||||
$this->titles = $head;
|
||||
if (! empty($this->sort_by)) {
|
||||
($this->sort_reverse) ? krsort($rows) : ksort($rows);
|
||||
if ($this->offset !== null || $this->limit !== null) {
|
||||
$rows = array_slice($rows, ($this->offset === null ? 0 : $this->offset), $this->limit, true);
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CSV data from array.
|
||||
*
|
||||
* @param data 2D array with data
|
||||
* @param fields field names
|
||||
* @param append if true, field names will not be output
|
||||
* @param is_php if a php die() call should be put on the first
|
||||
* line of the file, this is later ignored when read.
|
||||
* @param delimiter field delimiter to use
|
||||
* @param mixed $data
|
||||
* @param mixed $fields
|
||||
* @param mixed $append
|
||||
* @param mixed $is_php
|
||||
* @param null|mixed $delimiter
|
||||
*
|
||||
* @return CSV data (text string)
|
||||
*/
|
||||
public function unparse($data = [], $fields = [], $append = false, $is_php = false, $delimiter = null)
|
||||
{
|
||||
if (! is_array($data) || empty($data)) {
|
||||
$data = &$this->data;
|
||||
}
|
||||
if (! is_array($fields) || empty($fields)) {
|
||||
$fields = &$this->titles;
|
||||
}
|
||||
if ($delimiter === null) {
|
||||
$delimiter = $this->delimiter;
|
||||
}
|
||||
|
||||
$string = ($is_php) ? "<?php header('Status: 403'); die(' '); ?>".$this->linefeed : '';
|
||||
$entry = [];
|
||||
|
||||
// create heading
|
||||
if ($this->heading && ! $append) {
|
||||
foreach ($fields as $key => $value) {
|
||||
$entry[] = $this->_enclose_value($value);
|
||||
}
|
||||
$string .= implode($delimiter, $entry).$this->linefeed;
|
||||
$entry = [];
|
||||
}
|
||||
|
||||
// create data
|
||||
foreach ($data as $key => $row) {
|
||||
foreach ($row as $field => $value) {
|
||||
$entry[] = $this->_enclose_value($value);
|
||||
}
|
||||
$string .= implode($delimiter, $entry).$this->linefeed;
|
||||
$entry = [];
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load local file or string.
|
||||
*
|
||||
* @param input local CSV file
|
||||
* @param null|mixed $input
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public function load_data($input = null)
|
||||
{
|
||||
$data = null;
|
||||
$file = null;
|
||||
if ($input === null) {
|
||||
$file = $this->file;
|
||||
} elseif (file_exists($input)) {
|
||||
$file = $input;
|
||||
} else {
|
||||
$data = $input;
|
||||
}
|
||||
if (! empty($data) || $data = $this->_rfile($file)) {
|
||||
if ($this->file != $file) {
|
||||
$this->file = $file;
|
||||
}
|
||||
if (preg_match('/\.php$/i', $file) && preg_match('/<\?.*?\?>(.*)/ims', $data, $strip)) {
|
||||
$data = ltrim($strip[1]);
|
||||
}
|
||||
if ($this->convert_encoding) {
|
||||
$data = iconv($this->input_encoding, $this->output_encoding, $data);
|
||||
}
|
||||
if (substr($data, -1) != "\n") {
|
||||
$data .= "\n";
|
||||
}
|
||||
$this->file_data = &$data;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ----- [ Internal Functions ] -----------------
|
||||
// ==============================================
|
||||
|
||||
/**
|
||||
* Validate a row against specified conditions.
|
||||
*
|
||||
* @param row array with values from a row
|
||||
* @param conditions specified conditions that the row must match
|
||||
* @param mixed $row
|
||||
* @param null|mixed $conditions
|
||||
*
|
||||
* @return true of false
|
||||
*/
|
||||
public function _validate_row_conditions($row = [], $conditions = null)
|
||||
{
|
||||
if (! empty($row)) {
|
||||
if (! empty($conditions)) {
|
||||
$conditions = (strpos($conditions, ' OR ') !== false) ? explode(' OR ', $conditions) : [$conditions];
|
||||
$or = '';
|
||||
foreach ($conditions as $key => $value) {
|
||||
if (strpos($value, ' AND ') !== false) {
|
||||
$value = explode(' AND ', $value);
|
||||
$and = '';
|
||||
foreach ($value as $k => $v) {
|
||||
$and .= $this->_validate_row_condition($row, $v);
|
||||
}
|
||||
$or .= (strpos($and, '0') !== false) ? '0' : '1';
|
||||
} else {
|
||||
$or .= $this->_validate_row_condition($row, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return (strpos($or, '1') !== false) ? true : false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a row against a single condition.
|
||||
*
|
||||
* @param row array with values from a row
|
||||
* @param condition specified condition that the row must match
|
||||
* @param mixed $row
|
||||
* @param mixed $condition
|
||||
*
|
||||
* @return true of false
|
||||
*/
|
||||
public function _validate_row_condition($row, $condition)
|
||||
{
|
||||
$operators = [
|
||||
'=', 'equals', 'is',
|
||||
'!=', 'is not',
|
||||
'<', 'is less than',
|
||||
'>', 'is greater than',
|
||||
'<=', 'is less than or equals',
|
||||
'>=', 'is greater than or equals',
|
||||
'contains',
|
||||
'does not contain',
|
||||
];
|
||||
$operators_regex = [];
|
||||
foreach ($operators as $value) {
|
||||
$operators_regex[] = preg_quote($value, '/');
|
||||
}
|
||||
$operators_regex = implode('|', $operators_regex);
|
||||
if (preg_match('/^(.+) ('.$operators_regex.') (.+)$/i', trim($condition), $capture)) {
|
||||
$field = $capture[1];
|
||||
$op = $capture[2];
|
||||
$value = $capture[3];
|
||||
if (preg_match('/^([\'\"]{1})(.*)([\'\"]{1})$/i', $value, $capture)) {
|
||||
if ($capture[1] == $capture[3]) {
|
||||
$value = $capture[2];
|
||||
$value = str_replace('\\n', "\n", $value);
|
||||
$value = str_replace('\\r', "\r", $value);
|
||||
$value = str_replace('\\t', "\t", $value);
|
||||
$value = stripslashes($value);
|
||||
}
|
||||
}
|
||||
if (array_key_exists($field, $row)) {
|
||||
if (($op == '=' || $op == 'equals' || $op == 'is') && $row[$field] == $value) {
|
||||
return '1';
|
||||
} elseif (($op == '!=' || $op == 'is not') && $row[$field] != $value) {
|
||||
return '1';
|
||||
} elseif (($op == '<' || $op == 'is less than') && $row[$field] < $value) {
|
||||
return '1';
|
||||
} elseif (($op == '>' || $op == 'is greater than') && $row[$field] > $value) {
|
||||
return '1';
|
||||
} elseif (($op == '<=' || $op == 'is less than or equals') && $row[$field] <= $value) {
|
||||
return '1';
|
||||
} elseif (($op == '>=' || $op == 'is greater than or equals') && $row[$field] >= $value) {
|
||||
return '1';
|
||||
} elseif ($op == 'contains' && preg_match('/'.preg_quote($value, '/').'/i', $row[$field])) {
|
||||
return '1';
|
||||
} elseif ($op == 'does not contain' && ! preg_match('/'.preg_quote($value, '/').'/i', $row[$field])) {
|
||||
return '1';
|
||||
} else {
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the row is within the offset or not if sorting is disabled.
|
||||
*
|
||||
* @param current_row the current row number being processed
|
||||
* @param mixed $current_row
|
||||
*
|
||||
* @return true of false
|
||||
*/
|
||||
public function _validate_offset($current_row)
|
||||
{
|
||||
if ($this->sort_by === null && $this->offset !== null && $current_row < $this->offset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enclose values if needed
|
||||
* - only used by unparse().
|
||||
*
|
||||
* @param value string to process
|
||||
* @param null|mixed $value
|
||||
*
|
||||
* @return Processed value
|
||||
*/
|
||||
public function _enclose_value($value = null)
|
||||
{
|
||||
if ($value !== null && $value != '') {
|
||||
$delimiter = preg_quote($this->delimiter, '/');
|
||||
$enclosure = preg_quote($this->enclosure, '/');
|
||||
if (preg_match('/'.$delimiter.'|'.$enclosure."|\n|\r/i", $value) || ($value{0} == ' ' || substr($value, -1) == ' ')) {
|
||||
$value = str_replace($this->enclosure, $this->enclosure.$this->enclosure, $value);
|
||||
$value = $this->enclosure.$value.$this->enclosure;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check file data.
|
||||
*
|
||||
* @param file local filename
|
||||
* @param null|mixed $file
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public function _check_data($file = null)
|
||||
{
|
||||
if (empty($this->file_data)) {
|
||||
if ($file === null) {
|
||||
$file = $this->file;
|
||||
}
|
||||
|
||||
return $this->load_data($file);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed info might be delimiter
|
||||
* - only used by find_delimiter().
|
||||
*
|
||||
* @param mixed $char
|
||||
* @param mixed $array
|
||||
* @param mixed $depth
|
||||
* @param mixed $preferred
|
||||
*
|
||||
* @return special string used for delimiter selection, or false
|
||||
*/
|
||||
public function _check_count($char, $array, $depth, $preferred)
|
||||
{
|
||||
if ($depth == count($array)) {
|
||||
$first = null;
|
||||
$equal = null;
|
||||
$almost = false;
|
||||
foreach ($array as $key => $value) {
|
||||
if ($first == null) {
|
||||
$first = $value;
|
||||
} elseif ($value == $first && $equal !== false) {
|
||||
$equal = true;
|
||||
} elseif ($value == $first + 1 && $equal !== false) {
|
||||
$equal = true;
|
||||
$almost = true;
|
||||
} else {
|
||||
$equal = false;
|
||||
}
|
||||
}
|
||||
if ($equal) {
|
||||
$match = ($almost) ? 2 : 1;
|
||||
$pref = strpos($preferred, $char);
|
||||
$pref = ($pref !== false) ? str_pad($pref, 3, '0', STR_PAD_LEFT) : '999';
|
||||
|
||||
return $pref.$match.'.'.(99999 - str_pad($first, 5, '0', STR_PAD_LEFT));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read local file.
|
||||
*
|
||||
* @param file local filename
|
||||
* @param null|mixed $file
|
||||
*
|
||||
* @return Data from file, or false on failure
|
||||
*/
|
||||
public function _rfile($file = null)
|
||||
{
|
||||
if (is_readable($file)) {
|
||||
if (! ($fh = fopen($file, 'r'))) {
|
||||
return false;
|
||||
}
|
||||
$data = fread($fh, filesize($file));
|
||||
fclose($fh);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to local file.
|
||||
*
|
||||
* @param file local filename
|
||||
* @param string data to write to file
|
||||
* @param mode fopen() mode
|
||||
* @param lock flock() mode
|
||||
* @param mixed $file
|
||||
* @param mixed $string
|
||||
* @param mixed $mode
|
||||
* @param mixed $lock
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public function _wfile($file, $string = '', $mode = 'wb', $lock = 2)
|
||||
{
|
||||
if ($fp = fopen($file, $mode)) {
|
||||
flock($fp, $lock);
|
||||
$re = fwrite($fp, $string);
|
||||
$re2 = fclose($fp);
|
||||
if ($re != false && $re2 != false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -91,7 +91,7 @@ class ImportData extends Job implements ShouldQueue
|
||||
} catch (Exception $exception) {
|
||||
$subject = trans('texts.import_failed');
|
||||
$message = $exception->getMessage();
|
||||
Utils::logError($subject . ':' . $message);
|
||||
Utils::logError($subject . ': ' . $message);
|
||||
}
|
||||
|
||||
$userMailer->sendMessage($this->user, $subject, $message);
|
||||
|
@ -8,6 +8,7 @@ use App\Models\LookupAccount;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Exception;
|
||||
use App\Ninja\Mailers\UserMailer;
|
||||
|
||||
class PurgeAccountData extends Job
|
||||
{
|
||||
@ -16,7 +17,7 @@ class PurgeAccountData extends Job
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(UserMailer $userMailer)
|
||||
{
|
||||
$user = Auth::user();
|
||||
$account = $user->account;
|
||||
@ -73,5 +74,9 @@ class PurgeAccountData extends Job
|
||||
|
||||
config(['database.default' => $current]);
|
||||
}
|
||||
|
||||
$subject = trans('texts.purge_successful');
|
||||
$message = trans('texts.purge_details', ['account' => $user->account->getDisplayName()]);
|
||||
$userMailer->sendMessage($user, $subject, $message);
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,6 @@ class CurlUtils
|
||||
if ($response->getStatus() === 200) {
|
||||
return $response->getContent();
|
||||
} else {
|
||||
Utils::logError('Local PhantomJS Error: ' . $response->getStatus() . ' - ' . $url);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ class HTMLUtils
|
||||
$config->set('CSS.AllowImportant', true);
|
||||
$config->set('CSS.AllowTricky', true);
|
||||
$config->set('CSS.Trusted', true);
|
||||
$config->set('Cache.SerializerPath', base_path('storage/framework/cache'));
|
||||
|
||||
// Create a new purifier instance
|
||||
$purifier = new HTMLPurifier($config);
|
||||
@ -42,6 +43,8 @@ class HTMLUtils
|
||||
$html = html_entity_decode($html);
|
||||
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Cache.SerializerPath', base_path('storage/framework/cache'));
|
||||
|
||||
$purifier = new HTMLPurifier($config);
|
||||
|
||||
return $purifier->purify($html);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use DB;
|
||||
use App;
|
||||
use Auth;
|
||||
use Cache;
|
||||
@ -235,6 +236,26 @@ class Utils
|
||||
return App::getLocale() == 'en';
|
||||
}
|
||||
|
||||
public static function getDebugInfo()
|
||||
{
|
||||
if ($info = session('DEBUG_INFO')) {
|
||||
return $info;
|
||||
}
|
||||
|
||||
$mysqlVersion = DB::select( DB::raw("select version() as version") )[0]->version;
|
||||
$accountKey = Auth::check() ? Auth::user()->account->account_key : '';
|
||||
|
||||
$info = "App Version: v" . NINJA_VERSION . "\\n" .
|
||||
"White Label: " . (Utils::isWhiteLabel() ? 'Yes' : 'No') . " - {$accountKey}\\n" .
|
||||
"Server OS: " . php_uname('s') . ' ' . php_uname('r') . "\\n" .
|
||||
"PHP Version: " . phpversion() . "\\n" .
|
||||
"MySQL Version: " . $mysqlVersion;
|
||||
|
||||
session(['DEBUG_INFO' => $info]);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
public static function getLocaleRegion()
|
||||
{
|
||||
$parts = explode('_', App::getLocale());
|
||||
@ -410,6 +431,27 @@ class Utils
|
||||
];
|
||||
}
|
||||
|
||||
public static function getErrors()
|
||||
{
|
||||
$data = [];
|
||||
$filename = storage_path('logs/laravel-error.log');
|
||||
|
||||
if (! file_exists($filename)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$errors = file($filename);
|
||||
|
||||
for ($i=count($errors)-1; $i>=0; $i--) {
|
||||
$data[] = $errors[$i];
|
||||
if (count($data) >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function parseFloat($value)
|
||||
{
|
||||
$value = preg_replace('/[^0-9\.\-]/', '', $value);
|
||||
@ -1228,6 +1270,18 @@ class Utils
|
||||
return round($amount + $tax1 + $tax2, 2);
|
||||
}
|
||||
|
||||
public static function roundSignificant($value, $precision = 2) {
|
||||
if (round($value, 3) != $value) {
|
||||
$precision = 4;
|
||||
} elseif (round($value, 2) != $value) {
|
||||
$precision = 3;
|
||||
} elseif (round($value, 1) != $value) {
|
||||
$precision = 2;
|
||||
}
|
||||
|
||||
return number_format($value, $precision, '.', '');
|
||||
}
|
||||
|
||||
public static function truncateString($string, $length)
|
||||
{
|
||||
return strlen($string) > $length ? rtrim(substr($string, 0, $length)) . '...' : $string;
|
||||
|
@ -48,7 +48,7 @@ class InvoiceListener
|
||||
public function updatedInvoice(InvoiceWasUpdated $event)
|
||||
{
|
||||
$invoice = $event->invoice;
|
||||
$invoice->updatePaidStatus(false);
|
||||
$invoice->updatePaidStatus(false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,7 +71,7 @@ class InvoiceListener
|
||||
$partial = max(0, $invoice->partial - $payment->amount);
|
||||
|
||||
$invoice->updateBalances($adjustment, $partial);
|
||||
$invoice->updatePaidStatus();
|
||||
$invoice->updatePaidStatus(true);
|
||||
|
||||
// store a backup of the invoice
|
||||
$activity = Activity::wherePaymentId($payment->id)
|
||||
|
@ -226,6 +226,7 @@ class Account extends Eloquent
|
||||
|
||||
public static $customLabels = [
|
||||
'balance_due',
|
||||
'credit_card',
|
||||
'description',
|
||||
'discount',
|
||||
'due_date',
|
||||
@ -233,6 +234,7 @@ class Account extends Eloquent
|
||||
'id_number',
|
||||
'item',
|
||||
'line_total',
|
||||
'outstanding',
|
||||
'paid_to_date',
|
||||
'partial_due',
|
||||
'po_number',
|
||||
|
@ -35,4 +35,14 @@ class AccountEmailSettings extends Eloquent
|
||||
'late_fee3_percent',
|
||||
];
|
||||
|
||||
public static $templates = [
|
||||
TEMPLATE_INVOICE,
|
||||
TEMPLATE_QUOTE,
|
||||
//TEMPLATE_PARTIAL,
|
||||
TEMPLATE_PAYMENT,
|
||||
TEMPLATE_REMINDER1,
|
||||
TEMPLATE_REMINDER2,
|
||||
TEMPLATE_REMINDER3,
|
||||
];
|
||||
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ class AccountGateway extends EntityModel
|
||||
public static function paymentDriverClass($provider)
|
||||
{
|
||||
$folder = 'App\\Ninja\\PaymentDrivers\\';
|
||||
$provider = str_replace('\\', '', $provider);
|
||||
$class = $folder . $provider . 'PaymentDriver';
|
||||
$class = str_replace('_', '', $class);
|
||||
|
||||
@ -143,6 +144,22 @@ class AccountGateway extends EntityModel
|
||||
return ! empty($this->getConfigField('enableAch'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getAlipayEnabled()
|
||||
{
|
||||
return ! empty($this->getConfigField('enableAlipay'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getSofortEnabled()
|
||||
{
|
||||
return ! empty($this->getConfigField('enableSofort'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
@ -25,6 +25,16 @@ class AccountGatewayToken extends Eloquent
|
||||
*/
|
||||
protected $casts = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'contact_id',
|
||||
'account_gateway_id',
|
||||
'client_id',
|
||||
'token',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
@ -41,6 +51,14 @@ class AccountGatewayToken extends Eloquent
|
||||
return $this->belongsTo('App\Models\AccountGateway');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function contact()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Contact');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
@ -49,6 +67,14 @@ class AccountGatewayToken extends Eloquent
|
||||
return $this->hasOne('App\Models\PaymentMethod', 'id', 'default_payment_method_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEntityType()
|
||||
{
|
||||
return ENTITY_CUSTOMER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
@ -96,8 +122,10 @@ class AccountGatewayToken extends Eloquent
|
||||
} elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) {
|
||||
$merchantId = $accountGateway->getConfigField('merchantId');
|
||||
$testMode = $accountGateway->getConfigField('testMode');
|
||||
|
||||
return $testMode ? "https://sandbox.braintreegateway.com/merchants/{$merchantId}/customers/{$this->token}" : "https://www.braintreegateway.com/merchants/{$merchantId}/customers/{$this->token}";
|
||||
} elseif ($accountGateway->gateway_id == GATEWAY_GOCARDLESS) {
|
||||
$testMode = $accountGateway->getConfigField('testMode');
|
||||
return $testMode ? "https://manage-sandbox.gocardless.com/customers/{$this->token}" : "https://manage.gocardless.com/customers/{$this->token}";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -94,16 +94,16 @@ class Client extends EntityModel
|
||||
{
|
||||
return [
|
||||
'first' => 'contact_first_name',
|
||||
'last' => 'contact_last_name',
|
||||
'last^last4' => 'contact_last_name',
|
||||
'email' => 'contact_email',
|
||||
'work|office' => 'work_phone',
|
||||
'mobile|phone' => 'contact_phone',
|
||||
'name|organization' => 'name',
|
||||
'apt|street2|address2' => 'address2',
|
||||
'street|address|address1' => 'address1',
|
||||
'name|organization|description^card' => 'name',
|
||||
'apt|street2|address2|line2' => 'address2',
|
||||
'street|address1|line1^avs' => 'address1',
|
||||
'city' => 'city',
|
||||
'state|province' => 'state',
|
||||
'zip|postal|code' => 'postal_code',
|
||||
'zip|postal|code^avs' => 'postal_code',
|
||||
'country' => 'country',
|
||||
'public' => 'public_notes',
|
||||
'private|note' => 'private_notes',
|
||||
|
@ -190,6 +190,22 @@ class Company extends Eloquent
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function applyDiscount($amount)
|
||||
{
|
||||
$this->discount = $amount;
|
||||
$this->promo_expires = date_create()->modify('14 days')->format('Y-m-d');
|
||||
}
|
||||
|
||||
public function applyFreeYear()
|
||||
{
|
||||
$this->plan = PLAN_PRO;
|
||||
$this->plan_term = PLAN_TERM_YEARLY;
|
||||
$this->plan_price = PLAN_PRICE_PRO_MONTHLY;
|
||||
$this->plan_started = date_create()->format('Y-m-d');
|
||||
$this->plan_paid = date_create()->format('Y-m-d');
|
||||
$this->plan_expires = date_create()->modify('1 year')->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
Company::deleted(function ($company)
|
||||
|
@ -154,7 +154,7 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
|
||||
$url = trim(SITE_URL, '/');
|
||||
|
||||
if ($account->hasFeature(FEATURE_CUSTOM_URL)) {
|
||||
if (Utils::isNinjaProd()) {
|
||||
if (Utils::isNinjaProd() && ! Utils::isReseller()) {
|
||||
$url = $account->present()->clientPortalLink();
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ class Country extends Eloquent
|
||||
'swap_currency_symbol',
|
||||
'thousand_separator',
|
||||
'decimal_separator',
|
||||
'iso_3166_3',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -47,6 +47,7 @@ class Gateway extends Eloquent
|
||||
GATEWAY_BRAINTREE,
|
||||
GATEWAY_AUTHORIZE_NET,
|
||||
GATEWAY_MOLLIE,
|
||||
GATEWAY_GOCARDLESS,
|
||||
GATEWAY_CUSTOM,
|
||||
];
|
||||
|
||||
|
@ -82,7 +82,7 @@ class Invitation extends EntityModel
|
||||
}
|
||||
|
||||
if ($account->hasFeature(FEATURE_CUSTOM_URL)) {
|
||||
if (Utils::isNinjaProd()) {
|
||||
if (Utils::isNinjaProd() && ! Utils::isReseller()) {
|
||||
$url = $account->present()->clientPortalLink();
|
||||
}
|
||||
|
||||
|
@ -109,14 +109,16 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
'po_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
'amount',
|
||||
'paid',
|
||||
'notes',
|
||||
'terms',
|
||||
'product',
|
||||
'quantity',
|
||||
'tax1',
|
||||
'tax2',
|
||||
'public_notes',
|
||||
'private_notes',
|
||||
'item_product',
|
||||
'item_notes',
|
||||
'item_quantity',
|
||||
'item_cost',
|
||||
'item_tax1',
|
||||
'item_tax2',
|
||||
];
|
||||
}
|
||||
|
||||
@ -127,17 +129,19 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
{
|
||||
return [
|
||||
'number^po' => 'invoice_number',
|
||||
'amount' => 'amount',
|
||||
'client|organization' => 'name',
|
||||
'paid^date' => 'paid',
|
||||
'invoice date|create date' => 'invoice_date',
|
||||
'po number' => 'po_number',
|
||||
'due date' => 'due_date',
|
||||
'terms' => 'terms',
|
||||
'notes' => 'notes',
|
||||
'product|item' => 'product',
|
||||
'quantity|qty' => 'quantity',
|
||||
'tax' => 'tax1',
|
||||
'public notes' => 'public_notes',
|
||||
'private notes' => 'private_notes',
|
||||
'description' => 'item_notes',
|
||||
'quantity|qty' => 'item_quantity',
|
||||
'amount|cost' => 'item_cost',
|
||||
'product|item' => 'item_product',
|
||||
'tax' => 'item_tax1',
|
||||
];
|
||||
}
|
||||
|
||||
@ -357,6 +361,14 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
return $this->belongsTo('App\Models\Invoice');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function quote()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Invoice');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
@ -560,12 +572,12 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
/**
|
||||
* @param bool $save
|
||||
*/
|
||||
public function updatePaidStatus($save = true)
|
||||
public function updatePaidStatus($paid = false, $save = true)
|
||||
{
|
||||
$statusId = false;
|
||||
if ($this->amount != 0 && $this->balance == 0) {
|
||||
if ($paid && $this->balance == 0) {
|
||||
$statusId = INVOICE_STATUS_PAID;
|
||||
} elseif ($this->isSent() && $this->balance > 0 && $this->balance < $this->amount) {
|
||||
} elseif ($paid && $this->balance > 0 && $this->balance < $this->amount) {
|
||||
$statusId = INVOICE_STATUS_PARTIAL;
|
||||
} elseif ($this->isPartial() && $this->balance > 0) {
|
||||
$statusId = ($this->balance == $this->amount ? INVOICE_STATUS_SENT : INVOICE_STATUS_PARTIAL);
|
||||
@ -648,7 +660,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
|
||||
public function canBePaid()
|
||||
{
|
||||
return floatval($this->balance) != 0 && ! $this->is_deleted && $this->isStandard();
|
||||
return ! $this->isPaid() && ! $this->is_deleted && $this->isStandard();
|
||||
}
|
||||
|
||||
public static function calcStatusLabel($status, $class, $entityType, $quoteInvoiceId)
|
||||
@ -1175,10 +1187,17 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
$link = $invitation->getLink('view', true);
|
||||
$pdfString = false;
|
||||
$phantomjsSecret = env('PHANTOMJS_SECRET');
|
||||
$phantomjsLink = $link . "?phantomjs=true&phantomjs_secret={$phantomjsSecret}";
|
||||
|
||||
try {
|
||||
if (env('PHANTOMJS_BIN_PATH')) {
|
||||
$pdfString = CurlUtils::phantom('GET', $link . "?phantomjs=true&phantomjs_secret={$phantomjsSecret}");
|
||||
// we see occasional 408 errors
|
||||
for ($i=1; $i<=5; $i++) {
|
||||
$pdfString = CurlUtils::phantom('GET', $phantomjsLink);
|
||||
if ($pdfString) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $pdfString && ($key = env('PHANTOMJS_CLOUD_KEY'))) {
|
||||
@ -1188,12 +1207,12 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
|
||||
$pdfString = strip_tags($pdfString);
|
||||
} catch (\Exception $exception) {
|
||||
Utils::logError("PhantomJS - Failed to load: {$exception->getMessage()}");
|
||||
Utils::logError("PhantomJS - Failed to load {$phantomjsLink}: {$exception->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $pdfString || strlen($pdfString) < 200) {
|
||||
Utils::logError("PhantomJS - Invalid response: {$pdfString}");
|
||||
Utils::logError("PhantomJS - Invalid response {$phantomjsLink}: {$pdfString}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1201,7 +1220,7 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
if ($pdf = Utils::decodePDF($pdfString)) {
|
||||
return $pdf;
|
||||
} else {
|
||||
Utils::logError("PhantomJS - Unable to decode: {$pdfString}");
|
||||
Utils::logError("PhantomJS - Unable to decode {$phantomjsLink}: {$pdfString}");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@ -1452,6 +1471,17 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
{
|
||||
return $this->isQuote() ? 'valid_until' : 'due_date';
|
||||
}
|
||||
|
||||
public function onlyHasTasks()
|
||||
{
|
||||
foreach ($this->invoice_items as $item) {
|
||||
if ($item->invoice_item_type_id != INVOICE_ITEM_TYPE_TASK) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Invoice::creating(function ($invoice) {
|
||||
|
@ -96,7 +96,7 @@ class LookupModel extends Eloquent
|
||||
->first();
|
||||
}
|
||||
if (! $isFound) {
|
||||
abort(500, "Looked up {$className} not found: {$field} => {$value}");
|
||||
abort(404, "Looked up {$className} not found: {$field} => {$value}");
|
||||
}
|
||||
|
||||
Cache::put($key, $server, 120);
|
||||
|
@ -21,11 +21,26 @@ class PaymentMethod extends EntityModel
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['id'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'contact_id',
|
||||
'payment_type_id',
|
||||
'source_reference',
|
||||
'last4',
|
||||
'expiration',
|
||||
'email',
|
||||
'currency_id',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
|
@ -21,4 +21,37 @@ class PaymentType extends Eloquent
|
||||
{
|
||||
return $this->belongsTo('App\Models\GatewayType');
|
||||
}
|
||||
|
||||
public static function parseCardType($cardName)
|
||||
{
|
||||
$cardTypes = [
|
||||
'visa' => PAYMENT_TYPE_VISA,
|
||||
'americanexpress' => PAYMENT_TYPE_AMERICAN_EXPRESS,
|
||||
'amex' => PAYMENT_TYPE_AMERICAN_EXPRESS,
|
||||
'mastercard' => PAYMENT_TYPE_MASTERCARD,
|
||||
'discover' => PAYMENT_TYPE_DISCOVER,
|
||||
'jcb' => PAYMENT_TYPE_JCB,
|
||||
'dinersclub' => PAYMENT_TYPE_DINERS,
|
||||
'carteblanche' => PAYMENT_TYPE_CARTE_BLANCHE,
|
||||
'chinaunionpay' => PAYMENT_TYPE_UNIONPAY,
|
||||
'unionpay' => PAYMENT_TYPE_UNIONPAY,
|
||||
'laser' => PAYMENT_TYPE_LASER,
|
||||
'maestro' => PAYMENT_TYPE_MAESTRO,
|
||||
'solo' => PAYMENT_TYPE_SOLO,
|
||||
'switch' => PAYMENT_TYPE_SWITCH,
|
||||
];
|
||||
|
||||
$cardName = strtolower(str_replace([' ', '-', '_'], '', $cardName));
|
||||
|
||||
if (empty($cardTypes[$cardName]) && 1 == preg_match('/^('.implode('|', array_keys($cardTypes)).')/', $cardName, $matches)) {
|
||||
// Some gateways return extra stuff after the card name
|
||||
$cardName = $matches[1];
|
||||
}
|
||||
|
||||
if (! empty($cardTypes[$cardName])) {
|
||||
return $cardTypes[$cardName];
|
||||
} else {
|
||||
return PAYMENT_TYPE_CREDIT_CARD_OTHER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,12 @@ trait SendsEmails
|
||||
$entityType = 'reminder';
|
||||
}
|
||||
|
||||
return trans("texts.{$entityType}_subject", ['invoice' => '$invoice', 'account' => '$account', 'quote' => '$quote']);
|
||||
return trans("texts.{$entityType}_subject", [
|
||||
'invoice' => '$invoice',
|
||||
'account' => '$account',
|
||||
'quote' => '$quote',
|
||||
'number' => '$number',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,23 +83,23 @@ class InvoiceDatatable extends EntityDatatable
|
||||
|
||||
return [
|
||||
[
|
||||
trans("texts.edit_{$entityType}"),
|
||||
function ($model) use ($entityType) {
|
||||
return URL::to("{$entityType}s/{$model->public_id}/edit");
|
||||
},
|
||||
trans("texts.clone_invoice"),
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
},
|
||||
],
|
||||
[
|
||||
trans("texts.clone_{$entityType}"),
|
||||
function ($model) use ($entityType) {
|
||||
return URL::to("{$entityType}s/{$model->public_id}/clone");
|
||||
return URL::to("invoices/{$model->public_id}/clone");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('create', ENTITY_INVOICE);
|
||||
},
|
||||
],
|
||||
[
|
||||
trans("texts.clone_quote"),
|
||||
function ($model) {
|
||||
return URL::to("quotes/{$model->public_id}/clone");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('create', ENTITY_QUOTE);
|
||||
},
|
||||
],
|
||||
[
|
||||
trans('texts.view_history'),
|
||||
function ($model) use ($entityType) {
|
||||
@ -129,7 +129,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return "javascript:submitForm_{$entityType}('markPaid', {$model->public_id})";
|
||||
},
|
||||
function ($model) use ($entityType) {
|
||||
return $entityType == ENTITY_INVOICE && $model->balance != 0 && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
return $entityType == ENTITY_INVOICE && $model->invoice_status_id != INVOICE_STATUS_PAID && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -138,16 +138,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return URL::to("payments/create/{$model->client_public_id}/{$model->public_id}");
|
||||
},
|
||||
function ($model) use ($entityType) {
|
||||
return $entityType == ENTITY_INVOICE && $model->balance > 0 && Auth::user()->can('create', ENTITY_PAYMENT);
|
||||
},
|
||||
],
|
||||
[
|
||||
trans('texts.view_quote'),
|
||||
function ($model) {
|
||||
return URL::to("quotes/{$model->quote_id}/edit");
|
||||
},
|
||||
function ($model) use ($entityType) {
|
||||
return $entityType == ENTITY_INVOICE && $model->quote_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
return $entityType == ENTITY_INVOICE && $model->invoice_status_id != INVOICE_STATUS_PAID && Auth::user()->can('create', ENTITY_PAYMENT);
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -52,7 +52,7 @@ class PaymentDatatable extends EntityDatatable
|
||||
[
|
||||
'method',
|
||||
function ($model) {
|
||||
return ($model->payment_type && ! $model->last4) ? trans('texts.payment_type_' . $model->payment_type) : ($model->account_gateway_id ? $model->gateway_name : '');
|
||||
return $model->account_gateway_id ? $model->gateway_name : ($model->payment_type ? trans('texts.payment_type_' . $model->payment_type) : '');
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -67,6 +67,8 @@ class PaymentDatatable extends EntityDatatable
|
||||
return '<img height="22" src="' . URL::to('/images/credit_cards/' . $code . '.png') . '" alt="' . htmlentities($card_type) . '"> •••' . $model->last4 . ' ' . $expiration;
|
||||
} elseif ($model->email) {
|
||||
return $model->email;
|
||||
} elseif ($model->payment_type) {
|
||||
return trans('texts.payment_type_' . $model->payment_type);
|
||||
}
|
||||
} elseif ($model->last4) {
|
||||
if ($model->bank_name) {
|
||||
|
@ -105,7 +105,7 @@ class RecurringInvoiceDatatable extends EntityDatatable
|
||||
},
|
||||
],
|
||||
[
|
||||
trans('texts.clone_invoice'),
|
||||
trans("texts.clone_invoice"),
|
||||
function ($model) {
|
||||
return URL::to("invoices/{$model->public_id}/clone");
|
||||
},
|
||||
@ -113,6 +113,15 @@ class RecurringInvoiceDatatable extends EntityDatatable
|
||||
return Auth::user()->can('create', ENTITY_INVOICE);
|
||||
},
|
||||
],
|
||||
[
|
||||
trans("texts.clone_quote"),
|
||||
function ($model) {
|
||||
return URL::to("quotes/{$model->public_id}/clone");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('create', ENTITY_QUOTE);
|
||||
},
|
||||
],
|
||||
|
||||
];
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class VendorDatatable extends EntityDatatable
|
||||
},
|
||||
],
|
||||
[
|
||||
'client_created_at',
|
||||
'created_at',
|
||||
function ($model) {
|
||||
return Utils::timestampToDateString(strtotime($model->created_at));
|
||||
},
|
||||
|
@ -114,6 +114,38 @@ class BaseTransformer extends TransformerAbstract
|
||||
return $product->$field ?: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function getContact($email)
|
||||
{
|
||||
$email = trim(strtolower($email));
|
||||
|
||||
if (! isset($this->maps['contact'][$email])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->maps['contact'][$email];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function getCustomer($key)
|
||||
{
|
||||
$key = trim($key);
|
||||
|
||||
if (! isset($this->maps['customer'][$key])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->maps['customer'][$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
*
|
||||
@ -215,7 +247,7 @@ class BaseTransformer extends TransformerAbstract
|
||||
*/
|
||||
public function getInvoiceNumber($number)
|
||||
{
|
||||
return str_pad(trim($number), 4, '0', STR_PAD_LEFT);
|
||||
return $number ? str_pad(trim($number), 4, '0', STR_PAD_LEFT) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,18 +33,19 @@ class InvoiceTransformer extends BaseTransformer
|
||||
'po_number' => $this->getString($data, 'po_number'),
|
||||
'terms' => $this->getString($data, 'terms'),
|
||||
'public_notes' => $this->getString($data, 'public_notes'),
|
||||
'private_notes' => $this->getString($data, 'private_notes'),
|
||||
'invoice_date_sql' => $this->getDate($data, 'invoice_date'),
|
||||
'due_date_sql' => $this->getDate($data, 'due_date'),
|
||||
'invoice_items' => [
|
||||
[
|
||||
'product_key' => $this->getString($data, 'product'),
|
||||
'notes' => $this->getString($data, 'notes') ?: $this->getProduct($data, 'product', 'notes', ''),
|
||||
'cost' => $this->getFloat($data, 'amount') ?: $this->getProduct($data, 'product', 'cost', 0),
|
||||
'qty' => $this->getFloat($data, 'quantity') ?: 1,
|
||||
'tax_name1' => $this->getTaxName($this->getString($data, 'tax1')),
|
||||
'tax_rate1' => $this->getTaxRate($this->getString($data, 'tax1')),
|
||||
'tax_name2' => $this->getTaxName($this->getString($data, 'tax2')),
|
||||
'tax_rate2' => $this->getTaxRate($this->getString($data, 'tax2')),
|
||||
'product_key' => $this->getString($data, 'item_product'),
|
||||
'notes' => $this->getString($data, 'item_notes') ?: $this->getProduct($data, 'item_product', 'notes', ''),
|
||||
'cost' => $this->getFloat($data, 'item_notes') ?: $this->getProduct($data, 'item_product', 'cost', 0),
|
||||
'qty' => $this->getFloat($data, 'item_quantity') ?: 1,
|
||||
'tax_name1' => $this->getTaxName($this->getString($data, 'item_tax1')) ?: $this->getProduct($data, 'item_product', 'tax_name1', ''),
|
||||
'tax_rate1' => $this->getTaxRate($this->getString($data, 'item_tax1')) ?: $this->getProduct($data, 'item_product', 'tax_rate1', 0),
|
||||
'tax_name2' => $this->getTaxName($this->getString($data, 'item_tax2')) ?: $this->getProduct($data, 'item_product', 'tax_name2', ''),
|
||||
'tax_rate2' => $this->getTaxRate($this->getString($data, 'item_tax2')) ?: $this->getProduct($data, 'item_product', 'tax_rate2', 0),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
54
app/Ninja/Import/Stripe/CustomerTransformer.php
Normal file
54
app/Ninja/Import/Stripe/CustomerTransformer.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Import\Stripe;
|
||||
|
||||
use App\Ninja\Import\BaseTransformer;
|
||||
use League\Fractal\Resource\Item;
|
||||
use App\Models\PaymentType;
|
||||
|
||||
/**
|
||||
* Class InvoiceTransformer.
|
||||
*/
|
||||
class CustomerTransformer extends BaseTransformer
|
||||
{
|
||||
/**
|
||||
* @param $data
|
||||
*
|
||||
* @return bool|Item
|
||||
*/
|
||||
public function transform($data)
|
||||
{
|
||||
if (! $contact = $this->getContact($data->email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = auth()->user()->account;
|
||||
$accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE);
|
||||
|
||||
if (! $accountGateway) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getCustomer($data->id) || $this->getCustomer($data->email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Item($data, function ($data) use ($account, $contact, $accountGateway) {
|
||||
return [
|
||||
'contact_id' => $contact->id,
|
||||
'client_id' => $contact->client_id,
|
||||
'account_gateway_id' => $accountGateway->id,
|
||||
'token' => $data->id,
|
||||
'payment_method' => [
|
||||
'contact_id' => $contact->id,
|
||||
'payment_type_id' => PaymentType::parseCardType($data->card_brand),
|
||||
'source_reference' => $data->card_id,
|
||||
'last4' => $data->card_last4,
|
||||
'expiration' => $data->card_exp_year . '-' . $data->card_exp_month . '-01',
|
||||
'email' => $contact->email,
|
||||
'currency_id' => $account->getCurrencyId(),
|
||||
]
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
@ -131,8 +131,8 @@ class UserMailer extends Mailer
|
||||
$data = $data ?: [];
|
||||
$data += [
|
||||
'userName' => $user->getDisplayName(),
|
||||
'primaryMessage' => $subject,
|
||||
'secondaryMessage' => $message,
|
||||
'primaryMessage' => $message,
|
||||
//'secondaryMessage' => $message,
|
||||
'invoiceLink' => $invoice ? $invoice->present()->multiAccountLink : false,
|
||||
];
|
||||
|
||||
|
@ -1,10 +1,17 @@
|
||||
<?php namespace App\Ninja\OAuth;
|
||||
|
||||
use App\Models\LookupUser;
|
||||
use App\Models\User;
|
||||
|
||||
class OAuth {
|
||||
|
||||
const SOCIAL_GOOGLE = 1;
|
||||
const SOCIAL_FACEBOOK = 2;
|
||||
const SOCIAL_GITHUB = 3;
|
||||
const SOCIAL_LINKEDIN = 4;
|
||||
|
||||
private $providerInstance;
|
||||
private $providerId;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@ -16,6 +23,7 @@ class OAuth {
|
||||
{
|
||||
case 'google';
|
||||
$this->providerInstance = new Providers\Google();
|
||||
$this->providerId = self::SOCIAL_GOOGLE;
|
||||
return $this;
|
||||
|
||||
default:
|
||||
@ -26,11 +34,16 @@ class OAuth {
|
||||
|
||||
public function getTokenResponse($token)
|
||||
{
|
||||
$email = null;
|
||||
$user = null;
|
||||
|
||||
$payload = $this->providerInstance->getTokenResponse($token);
|
||||
$oauthUserId = $this->providerInstance->harvestSubField($payload);
|
||||
|
||||
LookupUser::setServerByField('oauth_user_key', $this->providerId . '-' . $oauthUserId);
|
||||
|
||||
if($this->providerInstance)
|
||||
$user = User::where('email', $this->providerInstance->getTokenResponse($token))->first();
|
||||
$user = User::where('oauth_user_id', $oauthUserId)->where('oauth_provider_id', $this->providerId)->first();
|
||||
|
||||
|
||||
if ($user)
|
||||
return $user;
|
||||
|
@ -7,11 +7,7 @@ class Google implements ProviderInterface
|
||||
{
|
||||
|
||||
$client = new \Google_Client(['client_id' => env('GOOGLE_CLIENT_ID','')]);
|
||||
$payload = $client->verifyIdToken($token);
|
||||
if ($payload)
|
||||
return $this->harvestEmail($payload);
|
||||
else
|
||||
return null;
|
||||
return $client->verifyIdToken($token);
|
||||
}
|
||||
|
||||
public function harvestEmail($payload)
|
||||
@ -19,5 +15,8 @@ class Google implements ProviderInterface
|
||||
return $payload['email'];
|
||||
}
|
||||
|
||||
|
||||
public function harvestSubField($payload)
|
||||
{
|
||||
return $payload['sub']; // user ID
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +141,11 @@ class BasePaymentDriver
|
||||
$invoicRepo->setGatewayFee($this->invoice(), $this->gatewayType);
|
||||
}
|
||||
|
||||
// For these gateway types we use the API directrly rather than Omnipay
|
||||
if ($this->shouldUseSource()) {
|
||||
return $this->createSource();
|
||||
}
|
||||
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_TOKEN) || $gateway->is_offsite) {
|
||||
if (Session::has('error')) {
|
||||
Session::reflash();
|
||||
@ -515,6 +520,12 @@ class BasePaymentDriver
|
||||
];
|
||||
}
|
||||
|
||||
public function shouldUseSource()
|
||||
{
|
||||
// Use Omnipay by default
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function shouldCreateToken()
|
||||
{
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
|
||||
@ -947,7 +958,11 @@ class BasePaymentDriver
|
||||
$label = e($this->accountGateway->getConfigField('name'));
|
||||
} else {
|
||||
$url = $this->paymentUrl($gatewayTypeAlias);
|
||||
$label = trans("texts.{$gatewayTypeAlias}");
|
||||
if ($custom = $this->account()->getLabel($gatewayTypeAlias)) {
|
||||
$label = $custom;
|
||||
} else {
|
||||
$label = trans("texts.{$gatewayTypeAlias}");
|
||||
}
|
||||
}
|
||||
|
||||
$label .= $this->invoice()->present()->gatewayFee($gatewayTypeId);
|
||||
@ -999,39 +1014,6 @@ class BasePaymentDriver
|
||||
return $url;
|
||||
}
|
||||
|
||||
protected function parseCardType($cardName)
|
||||
{
|
||||
$cardTypes = [
|
||||
'visa' => PAYMENT_TYPE_VISA,
|
||||
'americanexpress' => PAYMENT_TYPE_AMERICAN_EXPRESS,
|
||||
'amex' => PAYMENT_TYPE_AMERICAN_EXPRESS,
|
||||
'mastercard' => PAYMENT_TYPE_MASTERCARD,
|
||||
'discover' => PAYMENT_TYPE_DISCOVER,
|
||||
'jcb' => PAYMENT_TYPE_JCB,
|
||||
'dinersclub' => PAYMENT_TYPE_DINERS,
|
||||
'carteblanche' => PAYMENT_TYPE_CARTE_BLANCHE,
|
||||
'chinaunionpay' => PAYMENT_TYPE_UNIONPAY,
|
||||
'unionpay' => PAYMENT_TYPE_UNIONPAY,
|
||||
'laser' => PAYMENT_TYPE_LASER,
|
||||
'maestro' => PAYMENT_TYPE_MAESTRO,
|
||||
'solo' => PAYMENT_TYPE_SOLO,
|
||||
'switch' => PAYMENT_TYPE_SWITCH,
|
||||
];
|
||||
|
||||
$cardName = strtolower(str_replace([' ', '-', '_'], '', $cardName));
|
||||
|
||||
if (empty($cardTypes[$cardName]) && 1 == preg_match('/^('.implode('|', array_keys($cardTypes)).')/', $cardName, $matches)) {
|
||||
// Some gateways return extra stuff after the card name
|
||||
$cardName = $matches[1];
|
||||
}
|
||||
|
||||
if (! empty($cardTypes[$cardName])) {
|
||||
return $cardTypes[$cardName];
|
||||
} else {
|
||||
return PAYMENT_TYPE_CREDIT_CARD_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleWebHook($input)
|
||||
{
|
||||
throw new Exception('Unsupported gateway');
|
||||
|
@ -7,6 +7,7 @@ use Exception;
|
||||
use Session;
|
||||
use Utils;
|
||||
use App\Models\GatewayType;
|
||||
use App\Models\PaymentType;
|
||||
|
||||
class BraintreePaymentDriver extends BasePaymentDriver
|
||||
{
|
||||
@ -108,7 +109,7 @@ class BraintreePaymentDriver extends BasePaymentDriver
|
||||
if ($tokenResponse->isSuccessful()) {
|
||||
$customerReference = $tokenResponse->getCustomerData()->id;
|
||||
} else {
|
||||
Utils::logError($tokenResponse->getMessage());
|
||||
Utils::logError('Failed to create Braintree customer: ' . $tokenResponse->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -124,7 +125,7 @@ class BraintreePaymentDriver extends BasePaymentDriver
|
||||
if ($tokenResponse->isSuccessful()) {
|
||||
$this->tokenResponse = $tokenResponse->getData()->paymentMethod;
|
||||
} else {
|
||||
Utils::logError($tokenResponse->getMessage());
|
||||
Utils::logError('Failed to create Braintree token: ' . $tokenResponse->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -158,7 +159,7 @@ class BraintreePaymentDriver extends BasePaymentDriver
|
||||
$paymentMethod->source_reference = $response->token;
|
||||
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) {
|
||||
$paymentMethod->payment_type_id = $this->parseCardType($response->cardType);
|
||||
$paymentMethod->payment_type_id = PaymentType::parseCardType($response->cardType);
|
||||
$paymentMethod->last4 = $response->last4;
|
||||
$paymentMethod->expiration = $response->expirationYear . '-' . $response->expirationMonth . '-01';
|
||||
} elseif ($this->isGatewayType(GATEWAY_TYPE_PAYPAL)) {
|
||||
@ -211,4 +212,15 @@ class BraintreePaymentDriver extends BasePaymentDriver
|
||||
->send()
|
||||
->getToken();
|
||||
}
|
||||
|
||||
public function isValid()
|
||||
{
|
||||
try {
|
||||
$this->createTransactionToken();
|
||||
return true;
|
||||
} catch (Exception $exception) {
|
||||
return get_class($exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
127
app/Ninja/PaymentDrivers/GoCardlessV2RedirectPaymentDriver.php
Normal file
127
app/Ninja/PaymentDrivers/GoCardlessV2RedirectPaymentDriver.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\PaymentDrivers;
|
||||
|
||||
use Session;
|
||||
use App\Models\Payment;
|
||||
|
||||
class GoCardlessV2RedirectPaymentDriver extends BasePaymentDriver
|
||||
{
|
||||
protected $transactionReferenceParam = "\x00*\x00id";
|
||||
|
||||
public function gatewayTypes()
|
||||
{
|
||||
$types = [
|
||||
GATEWAY_TYPE_BANK_TRANSFER,
|
||||
GATEWAY_TYPE_TOKEN,
|
||||
];
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
protected function paymentDetails($paymentMethod = false)
|
||||
{
|
||||
$data = parent::paymentDetails($paymentMethod);
|
||||
|
||||
if ($paymentMethod) {
|
||||
$data['mandate_reference'] = $paymentMethod->source_reference;
|
||||
}
|
||||
|
||||
if ($ref = request()->redirect_flow_id) {
|
||||
$data['transaction_reference'] = $ref;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function shouldCreateToken()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function completeOffsitePurchase($input)
|
||||
{
|
||||
$details = $this->paymentDetails();
|
||||
$this->purchaseResponse = $response = $this->gateway()->completePurchase($details)->send();
|
||||
|
||||
if (! $response->isSuccessful()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$paymentMethod = $this->createToken();
|
||||
$payment = $this->completeOnsitePurchase(false, $paymentMethod);
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
protected function creatingCustomer($customer)
|
||||
{
|
||||
$customer->token = $this->purchaseResponse->getCustomerId();
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
protected function creatingPaymentMethod($paymentMethod)
|
||||
{
|
||||
$paymentMethod->source_reference = $this->purchaseResponse->getMandateId();
|
||||
$paymentMethod->payment_type_id = PAYMENT_TYPE_ACH;
|
||||
$paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED;
|
||||
|
||||
return $paymentMethod;
|
||||
}
|
||||
|
||||
protected function creatingPayment($payment, $paymentMethod)
|
||||
{
|
||||
$payment->payment_status_id = PAYMENT_STATUS_PENDING;
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
public function handleWebHook($input)
|
||||
{
|
||||
$accountGateway = $this->accountGateway;
|
||||
$accountId = $accountGateway->account_id;
|
||||
|
||||
$token = $accountGateway->getConfigField('webhookSecret');
|
||||
$rawPayload = file_get_contents('php://input');
|
||||
$providedSignature = $_SERVER['HTTP_WEBHOOK_SIGNATURE'];
|
||||
$calculatedSignature = hash_hmac('sha256', $rawPayload, $token);
|
||||
|
||||
if (! hash_equals($providedSignature, $calculatedSignature)) {
|
||||
throw new Exception('Signature does not match');
|
||||
}
|
||||
|
||||
foreach ($input['events'] as $event) {
|
||||
$type = $event['resource_type'];
|
||||
$action = $event['action'];
|
||||
|
||||
$supported = [
|
||||
'paid_out',
|
||||
'failed',
|
||||
'charged_back',
|
||||
];
|
||||
|
||||
if ($type != 'payments' || ! in_array($action, $supported)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceRef = isset($event['links']['payment']) ? $event['links']['payment'] : false;
|
||||
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $sourceRef)->first();
|
||||
|
||||
if (! $payment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($action == 'failed' || $action == 'charged_back') {
|
||||
if (! $payment->isFailed()) {
|
||||
$payment->markFailed($event['details']['description']);
|
||||
|
||||
$userMailer = app('App\Ninja\Mailers\UserMailer');
|
||||
$userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment);
|
||||
}
|
||||
} elseif ($action == 'paid_out') {
|
||||
$payment->markComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,12 @@
|
||||
namespace App\Ninja\PaymentDrivers;
|
||||
|
||||
use App\Models\Payment;
|
||||
use App\Models\Invitation;
|
||||
use App\Models\PaymentMethod;
|
||||
use App\Models\GatewayType;
|
||||
use Cache;
|
||||
use Exception;
|
||||
use App\Models\PaymentType;
|
||||
|
||||
class StripePaymentDriver extends BasePaymentDriver
|
||||
{
|
||||
@ -19,8 +22,30 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
GATEWAY_TYPE_TOKEN,
|
||||
];
|
||||
|
||||
if ($this->accountGateway && $this->accountGateway->getAchEnabled()) {
|
||||
$types[] = GATEWAY_TYPE_BANK_TRANSFER;
|
||||
if ($gateway = $this->accountGateway) {
|
||||
$achEnabled = $gateway->getAchEnabled();
|
||||
$sofortEnabled = $gateway->getSofortEnabled();
|
||||
if ($achEnabled && $sofortEnabled) {
|
||||
if ($this->invitation) {
|
||||
$country = ($this->client() && $this->client()->country) ? $this->client()->country->iso_3166_3 : ($this->account()->country ? $this->account()->country->iso_3166_3 : false);
|
||||
// https://stripe.com/docs/sources/sofort
|
||||
if ($country && in_array($country, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) {
|
||||
$types[] = GATEWAY_TYPE_SOFORT;
|
||||
} else {
|
||||
$types[] = GATEWAY_TYPE_BANK_TRANSFER;
|
||||
}
|
||||
} else {
|
||||
$types[] = GATEWAY_TYPE_BANK_TRANSFER;
|
||||
$types[] = GATEWAY_TYPE_SOFORT;
|
||||
}
|
||||
} elseif ($achEnabled) {
|
||||
$types[] = GATEWAY_TYPE_BANK_TRANSFER;
|
||||
} elseif ($sofortEnabled) {
|
||||
$types[] = GATEWAY_TYPE_SOFORT;
|
||||
}
|
||||
if ($gateway->getAlipayEnabled()) {
|
||||
$types[] = GATEWAY_TYPE_ALIPAY;
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
@ -57,6 +82,11 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
}
|
||||
}
|
||||
|
||||
public function shouldUseSource()
|
||||
{
|
||||
return in_array($this->gatewayType, [GATEWAY_TYPE_ALIPAY, GATEWAY_TYPE_SOFORT]);
|
||||
}
|
||||
|
||||
protected function checkCustomerExists($customer)
|
||||
{
|
||||
$response = $this->gateway()
|
||||
@ -189,7 +219,7 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
// In that case we'd use GATEWAY_TYPE_TOKEN even though we're creating the credit card
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD) || $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
|
||||
$paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01';
|
||||
$paymentMethod->payment_type_id = $this->parseCardType($source['brand']);
|
||||
$paymentMethod->payment_type_id = PaymentType::parseCardType($source['brand']);
|
||||
} elseif ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
|
||||
$paymentMethod->routing_number = $source['routing_number'];
|
||||
$paymentMethod->payment_type_id = PAYMENT_TYPE_ACH;
|
||||
@ -207,8 +237,17 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
|
||||
protected function creatingPayment($payment, $paymentMethod)
|
||||
{
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod)) {
|
||||
$isBank = $this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod);
|
||||
$isAlipay = $this->isGatewayType(GATEWAY_TYPE_ALIPAY, $paymentMethod);
|
||||
$isSofort = $this->isGatewayType(GATEWAY_TYPE_SOFORT, $paymentMethod);
|
||||
|
||||
if ($isBank || $isAlipay || $isSofort) {
|
||||
$payment->payment_status_id = $this->purchaseResponse['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING;
|
||||
if ($isAlipay) {
|
||||
$payment->payment_type_id = PAYMENT_TYPE_ALIPAY;
|
||||
} elseif ($isSofort) {
|
||||
$payment->payment_type_id = PAYMENT_TYPE_SOFORT;
|
||||
}
|
||||
}
|
||||
|
||||
return $payment;
|
||||
@ -307,6 +346,42 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createSource()
|
||||
{
|
||||
$amount = intval($this->invoice()->getRequestedAmount() * 100);
|
||||
$invoiceNumber = $this->invoice()->invoice_number;
|
||||
$currency = $this->client()->getCurrencyCode();
|
||||
$gatewayType = GatewayType::getAliasFromId($this->gatewayType);
|
||||
$redirect = url("/complete_source/{$this->invitation->invitation_key}/{$gatewayType}");
|
||||
$country = $this->client()->country ? $this->client()->country->iso_3166_2 : ($this->account()->country ? $this->account()->country->iso_3166_2 : '');
|
||||
$extra = '';
|
||||
|
||||
if ($this->gatewayType == GATEWAY_TYPE_ALIPAY) {
|
||||
if (! $this->accountGateway->getAlipayEnabled()) {
|
||||
throw new Exception('Alipay is not enabled');
|
||||
}
|
||||
$type = 'alipay';
|
||||
} else {
|
||||
if (! $this->accountGateway->getSofortEnabled()) {
|
||||
throw new Exception('Sofort is not enabled');
|
||||
}
|
||||
$type = 'sofort';
|
||||
$extra = "&sofort[country]={$country}&statement_descriptor={$invoiceNumber}";
|
||||
}
|
||||
|
||||
$data = "type={$type}&amount={$amount}¤cy={$currency}&redirect[return_url]={$redirect}{$extra}";
|
||||
$response = $this->makeStripeCall('POST', 'sources', $data);
|
||||
|
||||
if (is_array($response) && isset($response['id'])) {
|
||||
$this->invitation->transaction_reference = $response['id'];
|
||||
$this->invitation->save();
|
||||
|
||||
return redirect($response['redirect']['url']);
|
||||
} else {
|
||||
throw new Exception($response);
|
||||
}
|
||||
}
|
||||
|
||||
public function makeStripeCall($method, $url, $body = null)
|
||||
{
|
||||
$apiKey = $this->accountGateway->getConfig()->apiKey;
|
||||
@ -367,6 +442,7 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
'customer.source.updated',
|
||||
'customer.source.deleted',
|
||||
'customer.bank_account.deleted',
|
||||
'source.chargeable',
|
||||
];
|
||||
|
||||
if (! in_array($eventType, $supportedEvents)) {
|
||||
@ -388,11 +464,11 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded' || $eventType == 'charge.refunded') {
|
||||
$charge = $eventDetails['data']['object'];
|
||||
$transactionRef = $charge['id'];
|
||||
$source = $eventDetails['data']['object'];
|
||||
$sourceRef = $source['id'];
|
||||
|
||||
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first();
|
||||
if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded' || $eventType == 'charge.refunded') {
|
||||
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $sourceRef)->first();
|
||||
|
||||
if (! $payment) {
|
||||
return false;
|
||||
@ -400,7 +476,7 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
|
||||
if ($eventType == 'charge.failed') {
|
||||
if (! $payment->isFailed()) {
|
||||
$payment->markFailed($charge['failure_message']);
|
||||
$payment->markFailed($source['failure_message']);
|
||||
|
||||
$userMailer = app('App\Ninja\Mailers\UserMailer');
|
||||
$userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment);
|
||||
@ -408,12 +484,9 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
} elseif ($eventType == 'charge.succeeded') {
|
||||
$payment->markComplete();
|
||||
} elseif ($eventType == 'charge.refunded') {
|
||||
$payment->recordRefund($charge['amount_refunded'] / 100 - $payment->refunded);
|
||||
$payment->recordRefund($source['amount_refunded'] / 100 - $payment->refunded);
|
||||
}
|
||||
} elseif ($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted' || $eventType == 'customer.bank_account.deleted') {
|
||||
$source = $eventDetails['data']['object'];
|
||||
$sourceRef = $source['id'];
|
||||
|
||||
$paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first();
|
||||
|
||||
if (! $paymentMethod) {
|
||||
@ -425,6 +498,17 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
} elseif ($eventType == 'customer.source.updated') {
|
||||
//$this->paymentService->convertPaymentMethodFromStripe($source, null, $paymentMethod)->save();
|
||||
}
|
||||
} elseif ($eventType == 'source.chargeable') {
|
||||
$this->invitation = Invitation::scope(false, $accountId)->where('transaction_reference', '=', $sourceRef)->first();
|
||||
if (! $this->invitation) {
|
||||
return false;
|
||||
}
|
||||
$data = sprintf('amount=%d¤cy=%s&source=%s', $source['amount'], $source['currency'], $source['id']);
|
||||
$this->purchaseResponse = $response = $this->makeStripeCall('POST', 'charges', $data);
|
||||
$this->gatewayType = GatewayType::getIdFromAlias($source['type']);
|
||||
if (is_array($response) && isset($response['id'])) {
|
||||
$this->createPayment($response['id']);
|
||||
}
|
||||
}
|
||||
|
||||
return 'Processed successfully';
|
||||
|
@ -7,6 +7,7 @@ use App\Models\PaymentMethod;
|
||||
use Exception;
|
||||
use Session;
|
||||
use Utils;
|
||||
use App\Models\PaymentType;
|
||||
|
||||
class WePayPaymentDriver extends BasePaymentDriver
|
||||
{
|
||||
@ -159,7 +160,7 @@ class WePayPaymentDriver extends BasePaymentDriver
|
||||
}
|
||||
} else {
|
||||
$paymentMethod->last4 = $source->last_four;
|
||||
$paymentMethod->payment_type_id = $this->parseCardType($source->credit_card_name);
|
||||
$paymentMethod->payment_type_id = PaymentType::parseCardType($source->credit_card_name);
|
||||
$paymentMethod->expiration = $source->expiration_year . '-' . $source->expiration_month . '-01';
|
||||
$paymentMethod->source_reference = $source->credit_card_id;
|
||||
}
|
||||
|
@ -63,9 +63,16 @@ class AccountPresenter extends Presenter
|
||||
return $currency->code;
|
||||
}
|
||||
|
||||
public function clientPortalLink()
|
||||
public function clientPortalLink($subdomain = false)
|
||||
{
|
||||
return Domain::getLinkFromId($this->entity->domain_id);
|
||||
$account = $this->entity;
|
||||
$url = Domain::getLinkFromId($account->domain_id);
|
||||
|
||||
if ($subdomain && $account->subdomain) {
|
||||
$url = Utils::replaceSubdomain($url, $account->subdomain);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function industry()
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Ninja\Presenters;
|
||||
|
||||
use Utils;
|
||||
|
||||
class ClientPresenter extends EntityPresenter
|
||||
{
|
||||
public function country()
|
||||
@ -17,6 +19,19 @@ class ClientPresenter extends EntityPresenter
|
||||
return $account->formatMoney($client->balance, $client);
|
||||
}
|
||||
|
||||
public function websiteLink()
|
||||
{
|
||||
$client = $this->entity;
|
||||
|
||||
if (! $client->website) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$link = Utils::addHttp($client->website);
|
||||
|
||||
return link_to($link, $client->website, ['target' => '_blank']);
|
||||
}
|
||||
|
||||
public function paid_to_date()
|
||||
{
|
||||
$client = $this->entity;
|
||||
|
@ -8,6 +8,7 @@ use Carbon;
|
||||
use DropdownButton;
|
||||
use stdClass;
|
||||
use Utils;
|
||||
use Auth;
|
||||
|
||||
class InvoicePresenter extends EntityPresenter
|
||||
{
|
||||
@ -37,6 +38,14 @@ class InvoicePresenter extends EntityPresenter
|
||||
return $account->formatMoney($invoice->balance, $invoice->client);
|
||||
}
|
||||
|
||||
public function partial()
|
||||
{
|
||||
$invoice = $this->entity;
|
||||
$account = $invoice->account;
|
||||
|
||||
return $account->formatMoney($invoice->partial, $invoice->client);
|
||||
}
|
||||
|
||||
public function requestedAmount()
|
||||
{
|
||||
$invoice = $this->entity;
|
||||
@ -217,11 +226,16 @@ class InvoicePresenter extends EntityPresenter
|
||||
$entityType = $invoice->getEntityType();
|
||||
|
||||
$actions = [
|
||||
['url' => 'javascript:onCloneClick()', 'label' => trans("texts.clone_{$entityType}")],
|
||||
['url' => url("{$entityType}s/{$entityType}_history/{$invoice->public_id}"), 'label' => trans('texts.view_history')],
|
||||
DropdownButton::DIVIDER,
|
||||
['url' => 'javascript:onCloneInvoiceClick()', 'label' => trans("texts.clone_invoice")]
|
||||
];
|
||||
|
||||
if (Auth::user()->can('create', ENTITY_QUOTE)) {
|
||||
$actions[] = ['url' => 'javascript:onCloneQuoteClick()', 'label' => trans("texts.clone_quote")];
|
||||
}
|
||||
|
||||
$actions[] = ['url' => url("{$entityType}s/{$entityType}_history/{$invoice->public_id}"), 'label' => trans('texts.view_history')];
|
||||
$actions[] = DropdownButton::DIVIDER;
|
||||
|
||||
if ($entityType == ENTITY_QUOTE) {
|
||||
if ($invoice->quote_invoice_id) {
|
||||
$actions[] = ['url' => url("invoices/{$invoice->quote_invoice_id}/edit"), 'label' => trans('texts.view_invoice')];
|
||||
@ -229,11 +243,15 @@ class InvoicePresenter extends EntityPresenter
|
||||
$actions[] = ['url' => 'javascript:onConvertClick()', 'label' => trans('texts.convert_to_invoice')];
|
||||
}
|
||||
} elseif ($entityType == ENTITY_INVOICE) {
|
||||
if ($invoice->quote_id) {
|
||||
$actions[] = ['url' => url("quotes/{$invoice->quote_id}/edit"), 'label' => trans('texts.view_quote')];
|
||||
if ($invoice->quote_id && $invoice->quote) {
|
||||
$actions[] = ['url' => url("quotes/{$invoice->quote->public_id}/edit"), 'label' => trans('texts.view_quote')];
|
||||
}
|
||||
|
||||
if (!$invoice->deleted_at && ! $invoice->is_recurring && $invoice->balance != 0) {
|
||||
if ($invoice->onlyHasTasks()) {
|
||||
$actions[] = ['url' => 'javascript:onAddItemClick()', 'label' => trans('texts.add_item')];
|
||||
}
|
||||
|
||||
if ($invoice->canBePaid()) {
|
||||
$actions[] = ['url' => 'javascript:submitBulkAction("markPaid")', 'label' => trans('texts.mark_paid')];
|
||||
$actions[] = ['url' => 'javascript:onPaymentClick()', 'label' => trans('texts.enter_payment')];
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace App\Ninja\Reports;
|
||||
|
||||
use App\Models\Client;
|
||||
use Auth;
|
||||
use Utils;
|
||||
|
||||
class ProductReport extends AbstractReport
|
||||
{
|
||||
@ -48,8 +49,8 @@ class ProductReport extends AbstractReport
|
||||
$this->isExport ? $invoice->invoice_number : $invoice->present()->link,
|
||||
$invoice->present()->invoice_date,
|
||||
$item->product_key,
|
||||
$item->qty,
|
||||
$account->formatMoney($item->cost, $client),
|
||||
Utils::roundSignificant($item->qty, 0),
|
||||
Utils::roundSignificant($item->cost, 2),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,9 @@ class TaskReport extends AbstractReport
|
||||
foreach ($tasks->get() as $task) {
|
||||
$this->data[] = [
|
||||
$task->client ? ($this->isExport ? $task->client->getDisplayName() : $task->client->present()->link) : trans('texts.unassigned'),
|
||||
link_to($task->present()->url, $task->getStartTime()),
|
||||
$this->isExport ? $task->getStartTime() : link_to($task->present()->url, $task->getStartTime()),
|
||||
$task->present()->project,
|
||||
$task->present()->description,
|
||||
$task->description,
|
||||
Utils::formatTime($task->getDuration()),
|
||||
];
|
||||
}
|
||||
|
@ -42,17 +42,11 @@ class AccountRepository
|
||||
|
||||
if (Input::get('utm_campaign')) {
|
||||
if (env('PROMO_CAMPAIGN') && hash_equals(Input::get('utm_campaign'), env('PROMO_CAMPAIGN'))) {
|
||||
$company->discount = .75;
|
||||
$company->promo_expires = date_create()->modify('14 days')->format('Y-m-d');
|
||||
$company->applyDiscount(.75);
|
||||
}
|
||||
|
||||
if (env('PARTNER_CAMPAIGN') && hash_equals(Input::get('utm_campaign'), env('PARTNER_CAMPAIGN'))) {
|
||||
$company->plan = PLAN_PRO;
|
||||
$company->plan_term = PLAN_TERM_YEARLY;
|
||||
$company->plan_price = PLAN_PRICE_PRO_MONTHLY;
|
||||
$company->plan_started = date_create()->format('Y-m-d');
|
||||
$company->plan_paid = date_create()->format('Y-m-d');
|
||||
$company->plan_expires = date_create()->modify('1 year')->format('Y-m-d');
|
||||
$company->applyFreeYear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,6 +117,11 @@ class ClientRepository extends BaseRepository
|
||||
}
|
||||
}
|
||||
|
||||
// set default payment terms
|
||||
if (auth()->check() && ! isset($data['payment_terms'])) {
|
||||
$data['payment_terms'] = auth()->user()->account->payment_terms;
|
||||
}
|
||||
|
||||
$client->fill($data);
|
||||
$client->save();
|
||||
|
||||
|
@ -6,6 +6,13 @@ use App\Models\Contact;
|
||||
|
||||
class ContactRepository extends BaseRepository
|
||||
{
|
||||
public function all()
|
||||
{
|
||||
return Contact::scope()
|
||||
->withTrashed()
|
||||
->get();
|
||||
}
|
||||
|
||||
public function save($data, $contact = false)
|
||||
{
|
||||
$publicId = isset($data['public_id']) ? $data['public_id'] : false;
|
||||
|
42
app/Ninja/Repositories/CustomerRepository.php
Normal file
42
app/Ninja/Repositories/CustomerRepository.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Repositories;
|
||||
|
||||
use App\Models\PaymentMethod;
|
||||
use App\Models\AccountGatewayToken;
|
||||
use DB;
|
||||
|
||||
class CustomerRepository extends BaseRepository
|
||||
{
|
||||
public function getClassName()
|
||||
{
|
||||
return 'App\Models\AccountGatewayToken';
|
||||
}
|
||||
|
||||
public function all()
|
||||
{
|
||||
return AccountGatewayToken::whereAccountId(auth()->user()->account_id)
|
||||
->with(['contact'])
|
||||
->get();
|
||||
}
|
||||
|
||||
public function save($data)
|
||||
{
|
||||
$account = auth()->user()->account;
|
||||
|
||||
$customer = new AccountGatewayToken();
|
||||
$customer->account_id = $account->id;
|
||||
$customer->fill($data);
|
||||
$customer->save();
|
||||
|
||||
$paymentMethod = PaymentMethod::createNew();
|
||||
$paymentMethod->account_gateway_token_id = $customer->id;
|
||||
$paymentMethod->fill($data['payment_method']);
|
||||
$paymentMethod->save();
|
||||
|
||||
$customer->default_payment_method_id = $paymentMethod->id;
|
||||
$customer->save();
|
||||
|
||||
return $customer;
|
||||
}
|
||||
}
|
@ -526,8 +526,8 @@ class InvoiceRepository extends BaseRepository
|
||||
continue;
|
||||
}
|
||||
|
||||
$invoiceItemCost = round(Utils::parseFloat($item['cost']), 2);
|
||||
$invoiceItemQty = round(Utils::parseFloat($item['qty']), 2);
|
||||
$invoiceItemCost = Utils::roundSignificant(Utils::parseFloat($item['cost']));
|
||||
$invoiceItemQty = Utils::roundSignificant(Utils::parseFloat($item['qty']));
|
||||
|
||||
$lineTotal = $invoiceItemCost * $invoiceItemQty;
|
||||
$total += round($lineTotal, 2);
|
||||
@ -535,8 +535,8 @@ class InvoiceRepository extends BaseRepository
|
||||
|
||||
foreach ($data['invoice_items'] as $item) {
|
||||
$item = (array) $item;
|
||||
$invoiceItemCost = round(Utils::parseFloat($item['cost']), 2);
|
||||
$invoiceItemQty = round(Utils::parseFloat($item['qty']), 2);
|
||||
$invoiceItemCost = Utils::roundSignificant(Utils::parseFloat($item['cost']));
|
||||
$invoiceItemQty = Utils::roundSignificant(Utils::parseFloat($item['qty']));
|
||||
$lineTotal = $invoiceItemCost * $invoiceItemQty;
|
||||
|
||||
if ($invoice->discount > 0) {
|
||||
@ -815,11 +815,11 @@ class InvoiceRepository extends BaseRepository
|
||||
|
||||
/**
|
||||
* @param Invoice $invoice
|
||||
* @param null $quotePublicId
|
||||
* @param null $quoteId
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function cloneInvoice(Invoice $invoice, $quotePublicId = null)
|
||||
public function cloneInvoice(Invoice $invoice, $quoteId = null)
|
||||
{
|
||||
$invoice->load('invitations', 'invoice_items');
|
||||
$account = $invoice->account;
|
||||
@ -873,9 +873,9 @@ class InvoiceRepository extends BaseRepository
|
||||
$clone->$field = $invoice->$field;
|
||||
}
|
||||
|
||||
if ($quotePublicId) {
|
||||
if ($quoteId) {
|
||||
$clone->invoice_type_id = INVOICE_TYPE_STANDARD;
|
||||
$clone->quote_id = $quotePublicId;
|
||||
$clone->quote_id = $quoteId;
|
||||
if ($account->invoice_terms) {
|
||||
$clone->terms = $account->invoice_terms;
|
||||
}
|
||||
@ -890,7 +890,7 @@ class InvoiceRepository extends BaseRepository
|
||||
$clone->due_date = $account->defaultDueDate($invoice->client);
|
||||
$clone->save();
|
||||
|
||||
if ($quotePublicId) {
|
||||
if ($quoteId) {
|
||||
$invoice->quote_invoice_id = $clone->public_id;
|
||||
$invoice->save();
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ class ExpenseTransformer extends EntityTransformer
|
||||
* @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true)
|
||||
* @SWG\Property(property="archived_at", type="integer", example=1451160233, readOnly=true)
|
||||
* @SWG\Property(property="transaction_id", type="integer", example=1)
|
||||
* @SWG\Property(property="transaction_reference", type="string", example="")
|
||||
* @SWG\Property(property="bank_id", type="integer", example=1)
|
||||
* @SWG\Property(property="expense_currency_id", type="integer", example=1)
|
||||
* @SWG\Property(property="expense_category_id", type="integer", example=1)
|
||||
@ -67,6 +68,7 @@ class ExpenseTransformer extends EntityTransformer
|
||||
'updated_at' => $this->getTimestamp($expense->updated_at),
|
||||
'archived_at' => $this->getTimestamp($expense->deleted_at),
|
||||
'transaction_id' => $expense->transaction_id,
|
||||
'transaction_reference' => $expense->transaction_reference,
|
||||
'bank_id' => $expense->bank_id,
|
||||
'expense_currency_id' => (int) $expense->expense_currency_id,
|
||||
'expense_category_id' => $expense->expense_category ? (int) $expense->expense_category->public_id : null,
|
||||
|
@ -18,6 +18,7 @@ class TaskTransformer extends EntityTransformer
|
||||
*/
|
||||
protected $availableIncludes = [
|
||||
'client',
|
||||
'project',
|
||||
];
|
||||
|
||||
public function __construct(Account $account)
|
||||
@ -36,6 +37,17 @@ class TaskTransformer extends EntityTransformer
|
||||
}
|
||||
}
|
||||
|
||||
public function includeProject(Task $task)
|
||||
{
|
||||
if ($task->project) {
|
||||
$transformer = new ProjectTransformer($this->account, $this->serializer);
|
||||
|
||||
return $this->includeItem($task->project, $transformer, 'project');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function transform(Task $task)
|
||||
{
|
||||
return array_merge($this->getDefaults($task), [
|
||||
|
7
app/Policies/CustomerPolicy.php
Normal file
7
app/Policies/CustomerPolicy.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class CustomerPolicy extends EntityPolicy
|
||||
{
|
||||
}
|
@ -31,6 +31,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
\App\Models\BankAccount::class => \App\Policies\BankAccountPolicy::class,
|
||||
\App\Models\PaymentTerm::class => \App\Policies\PaymentTermPolicy::class,
|
||||
\App\Models\Project::class => \App\Policies\ProjectPolicy::class,
|
||||
\App\Models\AccountGatewayToken::class => \App\Policies\CustomerPolicy::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Contact;
|
||||
use App\Models\EntityModel;
|
||||
use App\Models\Expense;
|
||||
use App\Models\ExpenseCategory;
|
||||
@ -10,8 +11,10 @@ use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Product;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\AccountGatewayToken;
|
||||
use App\Ninja\Import\BaseTransformer;
|
||||
use App\Ninja\Repositories\ClientRepository;
|
||||
use App\Ninja\Repositories\CustomerRepository;
|
||||
use App\Ninja\Repositories\ContactRepository;
|
||||
use App\Ninja\Repositories\ExpenseCategoryRepository;
|
||||
use App\Ninja\Repositories\ExpenseRepository;
|
||||
@ -53,6 +56,11 @@ class ImportService
|
||||
*/
|
||||
protected $clientRepo;
|
||||
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
protected $customerRepo;
|
||||
|
||||
/**
|
||||
* @var ContactRepository
|
||||
*/
|
||||
@ -90,6 +98,7 @@ class ImportService
|
||||
ENTITY_TASK,
|
||||
ENTITY_PRODUCT,
|
||||
ENTITY_EXPENSE,
|
||||
ENTITY_CUSTOMER,
|
||||
];
|
||||
|
||||
/**
|
||||
@ -104,6 +113,7 @@ class ImportService
|
||||
IMPORT_INVOICEPLANE,
|
||||
IMPORT_NUTCACHE,
|
||||
IMPORT_RONIN,
|
||||
IMPORT_STRIPE,
|
||||
IMPORT_WAVE,
|
||||
IMPORT_ZOHO,
|
||||
];
|
||||
@ -113,6 +123,7 @@ class ImportService
|
||||
*
|
||||
* @param Manager $manager
|
||||
* @param ClientRepository $clientRepo
|
||||
* @param CustomerRepository $customerRepo
|
||||
* @param InvoiceRepository $invoiceRepo
|
||||
* @param PaymentRepository $paymentRepo
|
||||
* @param ContactRepository $contactRepo
|
||||
@ -121,6 +132,7 @@ class ImportService
|
||||
public function __construct(
|
||||
Manager $manager,
|
||||
ClientRepository $clientRepo,
|
||||
CustomerRepository $customerRepo,
|
||||
InvoiceRepository $invoiceRepo,
|
||||
PaymentRepository $paymentRepo,
|
||||
ContactRepository $contactRepo,
|
||||
@ -134,6 +146,7 @@ class ImportService
|
||||
$this->fractal->setSerializer(new ArraySerializer());
|
||||
|
||||
$this->clientRepo = $clientRepo;
|
||||
$this->customerRepo = $customerRepo;
|
||||
$this->invoiceRepo = $invoiceRepo;
|
||||
$this->paymentRepo = $paymentRepo;
|
||||
$this->contactRepo = $contactRepo;
|
||||
@ -428,8 +441,10 @@ class ImportService
|
||||
$entity = $this->{"{$entityType}Repo"}->save($data);
|
||||
|
||||
// update the entity maps
|
||||
$mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps';
|
||||
$this->$mapFunction($entity);
|
||||
if ($entityType != ENTITY_CUSTOMER) {
|
||||
$mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps';
|
||||
$this->$mapFunction($entity);
|
||||
}
|
||||
|
||||
// if the invoice is paid we'll also create a payment record
|
||||
if ($entityType === ENTITY_INVOICE && isset($data['paid']) && $data['paid'] > 0) {
|
||||
@ -632,14 +647,8 @@ class ImportService
|
||||
|
||||
private function getCsvData($fileName)
|
||||
{
|
||||
require_once app_path().'/Includes/parsecsv.lib.php';
|
||||
|
||||
$this->checkForFile($fileName);
|
||||
|
||||
$csv = new parseCSV();
|
||||
$csv->heading = false;
|
||||
$csv->auto($fileName);
|
||||
$data = $csv->data;
|
||||
$data = array_map('str_getcsv', file($fileName));
|
||||
|
||||
if (count($data) > 0) {
|
||||
$headers = $data[0];
|
||||
@ -842,6 +851,8 @@ class ImportService
|
||||
|
||||
$this->maps = [
|
||||
'client' => [],
|
||||
'contact' => [],
|
||||
'customer' => [],
|
||||
'invoice' => [],
|
||||
'invoice_client' => [],
|
||||
'product' => [],
|
||||
@ -861,6 +872,16 @@ class ImportService
|
||||
$this->addClientToMaps($client);
|
||||
}
|
||||
|
||||
$customers = $this->customerRepo->all();
|
||||
foreach ($customers as $customer) {
|
||||
$this->addCustomerToMaps($customer);
|
||||
}
|
||||
|
||||
$contacts = $this->contactRepo->all();
|
||||
foreach ($contacts as $contact) {
|
||||
$this->addContactToMaps($contact);
|
||||
}
|
||||
|
||||
$invoices = $this->invoiceRepo->all();
|
||||
foreach ($invoices as $invoice) {
|
||||
$this->addInvoiceToMaps($invoice);
|
||||
@ -927,6 +948,25 @@ class ImportService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
*/
|
||||
private function addCustomerToMaps(AccountGatewayToken $customer)
|
||||
{
|
||||
$this->maps['customer'][$customer->token] = $customer;
|
||||
$this->maps['customer'][$customer->contact->email] = $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Product $product
|
||||
*/
|
||||
private function addContactToMaps(Contact $contact)
|
||||
{
|
||||
if ($key = strtolower(trim($contact->email))) {
|
||||
$this->maps['contact'][$key] = $contact;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Product $product
|
||||
*/
|
||||
|
@ -54,6 +54,8 @@ class TemplateService
|
||||
'$balance' => $invoice->present()->balance,
|
||||
'$invoice' => $invoice->invoice_number,
|
||||
'$quote' => $invoice->invoice_number,
|
||||
'$number' => $invoice->invoice_number,
|
||||
'$partial' => $invoice->present()->partial,
|
||||
'$link' => $invitation->getLink(),
|
||||
'$password' => $passwordHTML,
|
||||
'$viewLink' => $invitation->getLink().'$password',
|
||||
|
@ -58,11 +58,12 @@ if (strstr($_SERVER['HTTP_USER_AGENT'], 'PhantomJS') && Utils::isNinjaDev()) {
|
||||
}
|
||||
*/
|
||||
|
||||
// Write info messages to a separate file
|
||||
$app->configureMonologUsing(function($monolog) {
|
||||
$monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-info.log', Monolog\Logger::INFO, false));
|
||||
$monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-warning.log', Monolog\Logger::WARNING, false));
|
||||
$monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-error.log', Monolog\Logger::ERROR, false));
|
||||
if (config('app.log') == 'single') {
|
||||
$monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-info.log', Monolog\Logger::INFO, false));
|
||||
$monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-warning.log', Monolog\Logger::WARNING, false));
|
||||
$monolog->pushHandler(new Monolog\Handler\StreamHandler(storage_path() . '/logs/laravel-error.log', Monolog\Logger::ERROR, false));
|
||||
}
|
||||
});
|
||||
|
||||
// Capture real IP if using cloudflare
|
||||
|
@ -37,6 +37,7 @@
|
||||
"descubraomundo/omnipay-pagarme": "dev-master",
|
||||
"digitickets/omnipay-barclays-epdq": "~3.0",
|
||||
"digitickets/omnipay-datacash": "~3.0",
|
||||
"digitickets/omnipay-gocardlessv2": "dev-payment-fix",
|
||||
"digitickets/omnipay-realex": "~5.0",
|
||||
"dioscouri/omnipay-cybersource": "dev-master",
|
||||
"doctrine/dbal": "2.5.x",
|
||||
@ -66,7 +67,7 @@
|
||||
"meebio/omnipay-creditcall": "dev-master",
|
||||
"meebio/omnipay-secure-trading": "dev-master",
|
||||
"mfauveau/omnipay-pacnet": "~2.0",
|
||||
"mpdf/mpdf": "^6.1",
|
||||
"mpdf/mpdf": "6.1.3",
|
||||
"nwidart/laravel-modules": "^1.14",
|
||||
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
|
||||
"omnipay/bitpay": "dev-master",
|
||||
@ -161,6 +162,10 @@
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/hillelcoren/l5-google-cloud-storage"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/hillelcoren/omnipay-gocardlessv2"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
602
composer.lock
generated
602
composer.lock
generated
File diff suppressed because it is too large
Load Diff
10
config/ninja.php
Normal file
10
config/ninja.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
// Hosted plan coupons
|
||||
'coupon_50_off' => env('COUPON_50_OFF', false),
|
||||
'coupon_75_off' => env('COUPON_75_OFF', false),
|
||||
'coupon_free_year' => env('COUPON_FREE_YEAR', false),
|
||||
|
||||
];
|
42
database/migrations/2017_08_14_085334_increase_precision.php
Normal file
42
database/migrations/2017_08_14_085334_increase_precision.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class IncreasePrecision extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('products', function ($table) {
|
||||
$table->decimal('cost', 15, 4)->change();
|
||||
$table->decimal('qty', 15, 4)->change();
|
||||
});
|
||||
|
||||
Schema::table('invoice_items', function ($table) {
|
||||
$table->decimal('cost', 15, 4)->change();
|
||||
$table->decimal('qty', 15, 4)->change();
|
||||
});
|
||||
|
||||
Schema::table('clients', function ($table) {
|
||||
$table->integer('credit_number_counter')->default(1)->nullable();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('clients', function ($table) {
|
||||
$table->dropColumn('credit_number_counter');
|
||||
});
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@ class GatewayTypesSeeder extends Seeder
|
||||
['alias' => 'bitcoin', 'name' => 'Bitcoin'],
|
||||
['alias' => 'dwolla', 'name' => 'Dwolla'],
|
||||
['alias' => 'custom', 'name' => 'Custom'],
|
||||
['alias' => 'alipay', 'name' => 'Alipay'],
|
||||
['alias' => 'sofort', 'name' => 'Sofort'],
|
||||
];
|
||||
|
||||
foreach ($gateway_types as $gateway_type) {
|
||||
|
@ -37,6 +37,7 @@ class LanguageSeeder extends Seeder
|
||||
['name' => 'Finnish', 'locale' => 'fi'],
|
||||
['name' => 'Romanian', 'locale' => 'ro'],
|
||||
['name' => 'Turkish - Turkey', 'locale' => 'tr_TR'],
|
||||
['name' => 'Thai', 'locale' => 'th'],
|
||||
];
|
||||
|
||||
foreach ($languages as $language) {
|
||||
|
@ -14,7 +14,7 @@ class PaymentLibrariesSeeder extends Seeder
|
||||
['name' => 'CardSave', 'provider' => 'CardSave'],
|
||||
['name' => 'Eway Rapid', 'provider' => 'Eway_RapidShared', 'is_offsite' => true],
|
||||
['name' => 'FirstData Connect', 'provider' => 'FirstData_Connect'],
|
||||
['name' => 'GoCardless', 'provider' => 'GoCardless', 'is_offsite' => true],
|
||||
['name' => 'GoCardless', 'provider' => 'GoCardless', 'is_offsite' => true, 'payment_library_id' => 2],
|
||||
['name' => 'Migs ThreeParty', 'provider' => 'Migs_ThreeParty'],
|
||||
['name' => 'Migs TwoParty', 'provider' => 'Migs_TwoParty'],
|
||||
['name' => 'Mollie', 'provider' => 'Mollie', 'is_offsite' => true, 'sort_order' => 7],
|
||||
@ -70,12 +70,15 @@ class PaymentLibrariesSeeder extends Seeder
|
||||
['name' => 'WeChat Express', 'provider' => 'WeChat_Express', 'payment_library_id' => 2],
|
||||
['name' => 'WePay', 'provider' => 'WePay', 'is_offsite' => false],
|
||||
['name' => 'Braintree', 'provider' => 'Braintree', 'sort_order' => 2],
|
||||
['name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 8],
|
||||
['name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 9],
|
||||
['name' => 'FirstData Payeezy', 'provider' => 'FirstData_Payeezy'],
|
||||
['name' => 'GoCardless', 'provider' => 'GoCardlessV2\Redirect', 'sort_order' => 8, 'is_offsite' => true],
|
||||
];
|
||||
|
||||
foreach ($gateways as $gateway) {
|
||||
$record = Gateway::where('name', '=', $gateway['name'])->first();
|
||||
$record = Gateway::whereName($gateway['name'])
|
||||
->whereProvider($gateway['provider'])
|
||||
->first();
|
||||
if ($record) {
|
||||
$record->fill($gateway);
|
||||
$record->save();
|
||||
|
@ -36,6 +36,8 @@ class PaymentTypesSeeder extends Seeder
|
||||
['name' => 'Swish', 'gateway_type_id' => GATEWAY_TYPE_BANK_TRANSFER],
|
||||
['name' => 'Venmo'],
|
||||
['name' => 'Money Order'],
|
||||
['name' => 'Alipay', 'gateway_type_id' => GATEWAY_TYPE_ALIPAY],
|
||||
['name' => 'Sofort', 'gateway_type_id' => GATEWAY_TYPE_SOFORT],
|
||||
];
|
||||
|
||||
foreach ($paymentTypes as $paymentType) {
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,9 +1,9 @@
|
||||
API
|
||||
===
|
||||
|
||||
Invoice Ninja provides a REST based API, `click here <https://app.invoiceninja.com/api-docs#/>`_ to see the full list of methods available.
|
||||
Invoice Ninja provides a RESTful API, `click here <https://app.invoiceninja.com/api-docs#/>`_ to see the full list of methods available.
|
||||
|
||||
To access the API you first need to create a token using the "Tokens” page under "Advanced Settings”.
|
||||
To access the API you first need to create a token using the "API Tokens” page under "Advanced Settings”.
|
||||
|
||||
- **Zapier** [hosted or self-host]: https://zapier.com/zapbook/invoice-ninja/
|
||||
- **Integromat**: https://www.integromat.com/en/integrations/invoiceninja
|
||||
@ -93,6 +93,7 @@ Options
|
||||
The following options are available when creating an invoice.
|
||||
|
||||
- ``email_invoice``: Email the invoice to the client.
|
||||
- ``email_type``: Set to reminder1, reminder2 or reminder3 to use the reminder template.
|
||||
- ``auto_bill``: Attempt to auto-bill the invoice using stored payment methods or credits.
|
||||
- ``paid``: Create a payment for the defined amount.
|
||||
|
||||
|
167
docs/clients.rst
167
docs/clients.rst
@ -8,9 +8,9 @@ Your clients are the core of your freelance business, and your Clients page is t
|
||||
List Clients
|
||||
""""""""""""
|
||||
|
||||
The Clients page is a list page that presents a summary of all your clients in a user-friendly table. Think of your Clients page as the “central station” of your invoicing activity. Most of your day-to-day invoicing actions can be taken from the various links and buttons that appear on the Clients list page. Now, we’ll take a closer look at the setup of the Clients page, and the range of actions available to you on the Clients page.
|
||||
The Clients page is a list page that presents a summary of all your clients in a user-friendly table. Think of your Clients page as the “central station” of your client activity. Most of your day-to-day invoicing actions can be taken from the various links and buttons that appear on the Clients list page. And you can use the Clients list page as your starting point to explore more in-depth client information, edit client information, view current client statements, and more. Now, we’ll take a closer look at the setup of the Clients page, and the range of actions available to you on the Clients page.
|
||||
|
||||
To view your client list page, go to the main taskbar and click the Clients tab.
|
||||
To view your client list page, go to the main sidebar and click the Clients tab.
|
||||
|
||||
Overview
|
||||
^^^^^^^^
|
||||
@ -20,8 +20,8 @@ The Clients page presents a list summary of all your current clients in a table
|
||||
- **Client:** The name of the client
|
||||
- **Contact:** The name of the primary contact person
|
||||
- **Email:** The client email address
|
||||
- **Date Created:** The date the client was created in the system
|
||||
- **Last Login:** The date an action was last taken for this client
|
||||
- **Date Created:** The date the client was created
|
||||
- **Last Login:** The date the client last logged in to the system
|
||||
- **Balance:** The client’s payment balance
|
||||
- **Action:** A range of actions you can take to manage activity relating to the selected client
|
||||
|
||||
@ -38,13 +38,10 @@ When you click on an action, you will be automatically redirected to the relevan
|
||||
- **New Quote** Enter a new quote on the Quotes / Create page
|
||||
- **Enter Payment** Enter a new payment on the Payments / Create page
|
||||
- **Enter Credit** Enter a new credit on the Credits / Create page
|
||||
- **Archive client** Click to archive the client
|
||||
- **Delete client** Click to delete the client
|
||||
- **Enter Expense** Enter a new expense on the Expenses / Create page
|
||||
- **Archive Client** Click to archive the client
|
||||
- **Delete Client** Click to delete the client
|
||||
|
||||
Credits
|
||||
^^^^^^^
|
||||
|
||||
You can manage your credits by visiting the Credits page directly from the Clients page. To open the Credits page, click on the gray Credits button that appears at the top right side of the page, to the left of the New Client + button.
|
||||
Sorting & Filtering Clients
|
||||
|
||||
The sort and filter functions make it easy for you to manage and view your client information.
|
||||
@ -53,21 +50,27 @@ Sort the clients table via any of the following data columns: Client, Contact, E
|
||||
|
||||
Filter the clients list by completing the Filter field, situated at the top right of the page, to the left of the gray Credits button. Clients can be filtered according to the client name, contact person name, or elements of the client name or contact person name. Here’s an example: Let’s filter the table for a client named “Joe Smith” of “Best Ninja” company. You can type “best ninja”, or “best” or “ninja”, or even “bes”, or “nin”, or “ja”, or “Joe”, “Smith”, “Jo” “oe”, “th” or any other grouping of letters in the client name or contact person name. The filter function will automatically locate and present all the relevant entries. This function makes it easy to find clients with even minimal input of information.
|
||||
|
||||
.. Tip:: Need to search for a specific client in your Clients list? Start typing the first letters of the client's name and the filter will automatically present the relevant listings.
|
||||
.. Tip: Need to search for a specific client in your Clients list? Start typing the first letters of the client's name and the filter will automatically present the relevant listings.
|
||||
|
||||
|
||||
Archiving/Deleting
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To archive or delete a specific client, hover over the client entry row, and open the Action drop-down list. Select Archive client or Delete client from the list. The Clients table will automatically refresh. Archived clients will appear in the table with a lighter gray font color, while deleted clients are hidden from view.
|
||||
To archive or delete a specific client, hover over the client entry row, and open the Action drop-down list. Select Archive client or Delete client from the list. The Clients table will automatically refresh.
|
||||
|
||||
- **Deleted clients** are displayed with a strike through and a red Deleted label in the Action column.
|
||||
- **Archived clients** are displayed with an orange Archived label in the Action column.
|
||||
|
||||
Note: You can also archive or delete one or more clients via the gray Archive button that appears at the top left side of the Clients table. To archive or delete clients, check the relevant clients in the check boxes that appear in the far left column next to the client name. Then click on the Archive button, open the drop-down list and select the desired action.
|
||||
|
||||
Want to view archived or deleted clients? Check the box marked Show archived/deleted clients, situated to the right of the Archive button. The table will automatically refresh, and will now feature the full list of clients, including current, archived and deleted clients.
|
||||
Want to view archived or deleted clients?
|
||||
Click on the tag field located on the left top side of the screen, to the right of the gray Archive button. When you click on the field, a drop down menu will open, displaying all the filter tags: Active, Archived and Deleted. Select the tags you want, and the table will update automatically to display the filtered client list.
|
||||
|
||||
- **Deleted clients** are displayed with a strike through.
|
||||
- **Archived clients** are displayed with a lighter gray font color.
|
||||
|
||||
You can choose to restore or delete the archived client. To restore an archived client, hover with your mouse over the Action area for the relevant archived client. A gray Select button will appear. Click on the Select arrow, and choose Restore client from the drop-down list. To delete an archived client, select Delete client from the drop-down list of the Select button.
|
||||
|
||||
To restore a deleted client, hover with your mouse over the Action area for the relevant deleted client. A gray Select button will appear. Click on the Select arrow, and choose Restore client from the drop-down list.
|
||||
|
||||
Create Client
|
||||
"""""""""""""
|
||||
|
||||
@ -75,43 +78,139 @@ So, you’ve taken on a new client? Congratulations!
|
||||
|
||||
Your Clients list is at the heart of your invoicing activity, so it's really important to maintain current information on all your clients. When you start working with a new client, the first thing you’ll need to do is to add the new client by entering their contact information and business details.
|
||||
|
||||
When creating and saving a new client to your Clients list, make sure to have the relevant, up-to-date information at hand. You are only required to enter the information one time. Invoice Ninja automatically tracks all invoicing activity for each client. Need to create an invoice, schedule a task or update a payment status? Simply select the client’s name from the Client list.
|
||||
When creating and saving a new client to your Clients list, make sure to have the relevant, up-to-date information at hand. You are only required to enter the information one time. Invoice Ninja automatically tracks all invoicing activity for each client.
|
||||
|
||||
There are two ways to enter a new client:
|
||||
|
||||
1. Via the Create Client page.
|
||||
2. Or, while creating a new invoice.
|
||||
|
||||
Here, we’re going to focus on entering a new client via the Create Client page.
|
||||
|
||||
**Let’s Begin**
|
||||
|
||||
To enter a new client, go to the Clients tab, open the drop-down menu, and click on New Client. This will open the Create Client page.
|
||||
To enter a new client, go to the Clients tab on the main sidebar, and click the + sign on the tab. This will open the Create Client page. Or, you can go to the Clients list page and click the blue New Client button at the top right side of the page.
|
||||
|
||||
The Create Client page is divided into four sections. Enter the information in the relevant fields.
|
||||
|
||||
.. Note:: You don’t have to complete every field. Enter the information that is important or necessary for your needs.
|
||||
.. Note: You don’t have to complete every field. Enter the information that is important or necessary for your needs.
|
||||
|
||||
Let’s take a closer look at each section:
|
||||
|
||||
- **Organization**: Enter details about your client’s business/company/organization, including the company name, ID number, VAT number, website address and telephone number.
|
||||
- **Contacts**: Enter the name, email address and phone number of your contact person for this client. You can enter as many contact people as you like. To add more contact people, click +Add Contact.
|
||||
- **Address**: Enter the street address of your client. This will be of particular importance if you need to send hard-copy invoices or payment receipts.
|
||||
- **Additional Info**: Enter the payment currency, language, payment due date, company size (no. of employees), the relevant industry sector, and any other private notes or reminders you wish to add (don’t worry - no one can see them but you.)
|
||||
- **Additional Info**: Enter the payment currency, language, payment terms, company size (no. of employees), the relevant industry sector, public notes (these will appear on the invoice by default) and private notes (don’t worry - no one can see them but you.)
|
||||
|
||||
TIP: Understanding the Payment terms field – You may have different payment terms and agreements for various clients. Here, you can select the default due date for the specific client via the drop-down menu of the Payment terms field. The default due date is calculated according to the date on the invoice. For example, Net 0 means the payment is due on the date of the invoice; Net 7 means the payment is due 7 days after the date of the invoice, and so on. Note: Even if you choose default payment terms, you can always manually adjust an invoice payment due date for a specific invoice, via the Edit Invoice page.
|
||||
|
||||
Once you have filled in the page, click Save to save the new client information. From now on, when you click the Client field, the client’s name will appear in the drop down menu. Simply select the client you need and let the invoicing begin!
|
||||
|
||||
Client Overview Page
|
||||
""""""""""""""""""""
|
||||
|
||||
Each client has its own Client Overview page. The overview page provides a complete summary of all your client details and activity in one page. From here, you can access everything you need about the specific client, including the client's general contact information, total standing payments and balance, and a detailed list of the client Activity, Invoices, Payments and Credits. You can also Edit, Archive or Delete the client, view the Client Statement and view the Client Portal, all directly from the Client Overview page. Let's explore:
|
||||
|
||||
How to view the Client Overview page
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To view the Client Overview page of a specific client, click on the client name in the Clients list page.
|
||||
|
||||
Understanding the Client Overview page
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The top section of the Client Overview page displays general business and contact information that you entered when creating the client, including contact name, street address, payment terms, email address, as well as standing payment and balance data. You can also view the client portal from here, by clicking on the View client portal link that appears below the client's email address.
|
||||
|
||||
.. TIP:: If you entered the client's street address, a Google map appears below the information box displaying the client's location.
|
||||
|
||||
Client Data Table
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Just below the client information box or Google map, you'll find the client's data table. The table displays a summary of your client's activity, invoices, payments and credits, in a simple accessible table format. It provides a fast summary of the data for the specific client only.
|
||||
|
||||
To access the various data tables, choose from the menu bar: Activity, Invoices, Payments or Credits.
|
||||
|
||||
Activity Table
|
||||
**************
|
||||
|
||||
The Activity table shows all the past activity with the client, in chronological order, with the most recent actions at the top. The table has 4 columns:
|
||||
|
||||
- **Date**: The date the action was taken
|
||||
- **Message**: The action that occurred
|
||||
- **Balance**: The client's current balance
|
||||
- **Adjustment**: The adjusted amount
|
||||
|
||||
Invoices Table
|
||||
**************
|
||||
|
||||
The Invoices table shows a list of all the client's invoices and accompanying information. The table has 5 columns:
|
||||
|
||||
- **Invoice**: The invoice number
|
||||
- **Date**: The date the invoice was created
|
||||
- **Amount**: The invoice amount
|
||||
- **Balance**: The invoice balance
|
||||
- **Due Date**: The date the payment is due
|
||||
- **Status**: The status of the invoice (Draft, Sent, Viewed, Paid, Overdue)
|
||||
|
||||
.. TIP:: You can also create a new invoice for this client via the blue New Invoice button that appears at the top right of the Invoices table.
|
||||
|
||||
Payments Table
|
||||
**************
|
||||
|
||||
The Payments table shows a list of all the client's payments and accompanying information. The table has 7 columns:
|
||||
|
||||
- **Invoice**: The invoice number
|
||||
- **Transaction reference**: The reference number of the transaction
|
||||
- **Method**: The payment method (ie. Paypal, manual entry, Amex, etc)
|
||||
- **Source**: The source of the payment
|
||||
- **Amount**: The payment amount
|
||||
- **Date**: The date the payment was made
|
||||
- **Status**: The status of the payment (ie. Pending, Completed, etc)
|
||||
|
||||
.. TIP:: You can also enter a payment for this client via the blue Enter Payment button that appears at the top right of the Payments table.
|
||||
|
||||
Credits Table
|
||||
*************
|
||||
|
||||
The Credits table shows a list of all the client's credits and accompanying information. The table has 5 columns:
|
||||
|
||||
- **Amount**: The credit amount
|
||||
- **Balance**: The current balance
|
||||
- **Credit Date**: The date the credit was issued
|
||||
- **Public Notes**: Comments entered by you (these will appear on the invoice)
|
||||
- **Private Notes**: Notes added by you (for your eyes only; the client cannot see these notes)
|
||||
|
||||
.. TIP:: You can also enter a credit for this client via the blue Enter Credit button that appears at the top right of the Credits table.
|
||||
|
||||
Clickable Links on the Client Overview Page
|
||||
*******************************************
|
||||
|
||||
The Client Overview page is rich in clickable links to the client's invoices, payments, credits and any other data pages relating to the client. So you can quickly look up any information with a simple click of an IP link. Take for example the Activity table: if you recently updated the client's invoice, an entry will appear on the Activity table, with a Message of: "You Updated Invoice 53". "Invoice 53" will appear as a clickable link that takes you directly to the invoice. So you have fast access to every relevant invoice, quote, task, expense and more, directly from every listing on the Activity table. This also applies to the Invoices, Payments and Credit tables. TIP: Any invoicing action you need to take for the client can be done from the Client Overview page.
|
||||
|
||||
Note: All the tables on the Client Overview page have Sort and Filter functions. You can filter by entering text into the filter field that appears above and to the right of the table. Also, each column can be sorted highest to lowest, or lowest to highest. Simply click on the small arrow that appears when you hover in the column heading field.
|
||||
|
||||
Client Statement
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
The client statement is a downloadable PDF document that provides a full and current statement of your client's balance.
|
||||
|
||||
**View Statement**: To view the client statement, click the blue View Statement button that appears at the top right side of the Client Overview page. This will automatically generate the PDF statement.
|
||||
|
||||
**Download Statement**: To download the PDF statement, click on the gray Download PDF button at the top right of the statement screen.
|
||||
|
||||
**Return to Client Overview**: To return to the client overview page, click the blue View Client button at the top right of the statement screen.
|
||||
|
||||
Actions on the View Statement Button
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can do all invoicing actions for the specific client with a click of the mouse, directly from the Client Overview page. Simply click on the arrow at the right hand side of the View Statement button. A drop-down menu will open, giving you quick access to all the actions: New Invoice, New Task, New Quote, New Recurring Invoice, Enter Payment, Enter Credit, Enter Expense.
|
||||
|
||||
How to Edit Client Information
|
||||
******************************
|
||||
|
||||
The information you enter on the Create Client page acts as your default settings for this client. You can change these settings at any time. How? There are two methods:
|
||||
Via the Clients list
|
||||
The information you enter on the Create Client page acts as your default settings for this client. You can change these settings at any time. How? By clicking on the gray Edit Client button on the Client Overview page.
|
||||
|
||||
1. Select the Clients tab to view your client list.
|
||||
2. Select the relevant client from the list. The summary page of the client will open.
|
||||
3. Click on the gray Edit Client button, at the top right corner of the page. You will now be taken to the Clients/Edit page, where you can edit any of the fields.
|
||||
Edit Client
|
||||
***********
|
||||
|
||||
During the invoicing process
|
||||
Click on the gray Edit Client button, at the top right corner of the page. You will now be taken to the Clients/Edit page, where you can edit any of the fields.
|
||||
|
||||
1. Open the New Invoice page.
|
||||
2. Click on the arrow at the right end of the Client field, and select the client name from the drop down menu.
|
||||
3. Click Edit Client, which appears below the Client field. This will open the Client window. You can now edit the client’s information.
|
||||
Archiving or Deleting the Client
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can also archive or delete the specific client directly from their Client Overview page.
|
||||
Click on the arrow at the right hand side of the Edit Client button. A drop-down menu will open, giving you the option to Archive Client or Delete Client.
|
||||
|
@ -57,9 +57,9 @@ author = u'Invoice Ninja'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'3.6'
|
||||
version = u'3.7'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'3.6.1'
|
||||
release = u'3.7.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -5,7 +5,7 @@ Let’s get acquainted with a basic overview of the structure of the Invoice Nin
|
||||
|
||||
The Invoice Ninja system is based on two main pillars:
|
||||
|
||||
- **List pages** are summary pages of the various activities in your account. These include the Clients list page, Tasks list page, Invoices list page and Payments list page. List pages are located in the main taskbar of the Invoice Ninja site. Simply click on Clients, Tasks, Invoices or Payments to open the list page you need. The list pages provide a centralized overview and management station for the particular component of your account. For example, the Clients list page is a list of all your clients, with accompanying information and handy links, so you can manage all your clients on one page.
|
||||
- **List pages** are summary pages of the various activities in your account. These include the Clients list page, Tasks list page, Invoices list page, Payments list page and more. List pages are located in the main sidebar of the Invoice Ninja site. The list pages provide a centralized overview and management station for the particular component of your account. For example, the Clients list page is a list of all your clients, with accompanying information and handy links, so you can manage all your clients on one page.
|
||||
|
||||
- **Action pages** are pages dedicated to a specific action you can take in your Invoice Ninja account. Examples include Create New Client, Enter Credit, Create New Invoice, Create Recurring Invoice, Enter Credit, Enter Payment and Create New Task. All actions you take will be recorded in the List pages, updated in real time to reflect your invoicing activity from minute to minute.
|
||||
|
||||
@ -20,9 +20,24 @@ Welcome to the Dashboard page of your Invoice Ninja account. This is the first p
|
||||
|
||||
So let’s jump right in and take a look at the different elements that make up your invoicing dashboard.
|
||||
|
||||
When you login to your Invoice Ninja account, you’ll automatically arrive on the Dashboard page. To go to the Dashboard page from anywhere in the site, click the Dashboard tab on the main taskbar.
|
||||
When you login to your Invoice Ninja account, you’ll automatically arrive on the Dashboard page. To go to the Dashboard page from anywhere in the site, click the Dashboard tab on the main sidebar at the left of your screen.
|
||||
|
||||
The first thing you’ll notice about the Dashboard page is the three large data boxes at the top of the screen. These are designed to offer a simple yet powerful overview of your total business accounts:
|
||||
Sidebar – Navigating Your Invoice Ninja Account
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
The Sidebar, at the left of your screen, is always visible, no matter where you are in your Invoice Ninja account. Use the sidebar to get where you want to go, quickly and easily.
|
||||
When you click on a specific tab, you'll go to the list page. So, if you click on Invoices, you'll navigate to the Invoices list page. Same for Quotes, Payments, Products, Recurring, Credits and many others.
|
||||
|
||||
Want to create a new invoice, quote, task, list, payment, or any other new item? Hover on the relevant tab, and click the blue plus sign that appears on the right hand side of the tab. You'll be automatically redirected to the "Create New…" page.
|
||||
|
||||
The last two tabs on the list are Reports and Settings. Click on Reports to navigate to the reporting page of your account. Here you can create all types of reports about your invoicing.
|
||||
|
||||
To manage any of your account settings, click on the Settings tab. Then you can work on the full range of account settings via the taskbar of the Settings page.
|
||||
|
||||
Data Boxes – Your Instant Invoicing Overview
|
||||
""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
One of the first things you’ll notice about the Dashboard page is the three large data boxes at the top of the screen. These are designed to offer a simple yet powerful overview of your total business accounts:
|
||||
|
||||
- **Total Revenue**: The total amount that your business has brought in to date.
|
||||
- **Average Invoice**: The amount of the current average invoice. Note this will change over time, depending upon your income.
|
||||
@ -30,14 +45,28 @@ The first thing you’ll notice about the Dashboard page is the three large data
|
||||
|
||||
.. TIP:: If you are being paid in a range of currencies, the three data boxes on your dashboard will display the relevant amounts in all currencies.
|
||||
|
||||
Below the three main data boxes, there are four ‘windows’ summarizing different aspects of your invoicing activity.
|
||||
Chart
|
||||
Below the three main data boxes, you'll see a chart presenting your invoicing data in an easy-to-understand graphical format. The data presented in the chart will be based upon the filters you select. Let's check out how the filters work.
|
||||
|
||||
Window 1: Notifications
|
||||
Filter
|
||||
""""""
|
||||
|
||||
The Dashboard page gives you a summary overview of your invoicing activity – and the filter option enables you to control exactly what data you are seeing. There are three filter options:
|
||||
|
||||
1. Currency: If you are working in various currencies, you can filter according to each currency to view your invoicing activity in only that currency.
|
||||
2. Day/Week/Month: For an overview of the past day, week or month, select the time period you prefer.
|
||||
3. Custom dates: Select any time period you want by filtering according to dates. Click on the custom date field, and you'll see a drop down menu with a range of pre-set options, such as "Last month", "This year", etc. To create a custom time frame, select "Custom range", choose your dates and click Apply.
|
||||
|
||||
When you apply the filter, the data presented in the boxes and chart will update automatically.
|
||||
|
||||
Below the chart, there are four ‘windows’ summarizing different aspects of your invoicing activity.
|
||||
|
||||
Window 1: Activity
|
||||
"""""""""""""""""""""""
|
||||
|
||||
The Notifications list is incredibly useful as it presents an up-to-date, action-packed summary of what is happening across your entire invoicing account. Every action taken, whether by you or by one of your clients, is listed in chronological order, together with the date the action occurred. The list is updated in real time, with more recent actions showing first, so you get a minute-to-minute understanding of your invoicing activity.
|
||||
The Activity list is incredibly useful as it presents an up-to-date, action-packed summary of what is happening across your entire invoicing account. Every action taken, whether by you or by one of your clients, is listed in chronological order, together with the date the action occurred. The list is updated in real time, with more recent actions showing first, so you get a minute-to-minute understanding of your invoicing activity.
|
||||
|
||||
The Notifications list includes all possible actions occurring within your Invoice Ninja account, including:
|
||||
The Activity list includes all possible actions occurring within your Invoice Ninja account, including:
|
||||
|
||||
- Creating an invoice
|
||||
- Sending an invoice
|
||||
@ -48,7 +77,7 @@ The Notifications list includes all possible actions occurring within your Invoi
|
||||
- Your client sending a payment
|
||||
- And many, many more
|
||||
|
||||
.. TIP:: You can view a real-time tally of the number of invoices sent, displayed at the top right side of the blue Notifications header bar.
|
||||
.. TIP:: You can view a real-time tally of the number of invoices sent, displayed at the top right side of the blue Activity header bar.
|
||||
|
||||
Window 2: Recent Payments
|
||||
"""""""""""""""""""""""""
|
||||
@ -81,7 +110,7 @@ The Invoices Past Due list provides a summary of all unpaid invoices. The Invoic
|
||||
- **Invoice #**: The invoice reference number
|
||||
- **Client**: The client’s name
|
||||
- **Due Date**: The original due date of the overdue payment
|
||||
- **Balance**: Due The amount overdue
|
||||
- **Balance Due**: The amount overdue
|
||||
|
||||
.. NOTE:: Archived invoices, payments and quotes will appear on the dashboard, and their amounts will be included in the account totals at the top of the page. Deleted invoices, payments and quotes will not appear, nor will their amounts be included on the Dashboard page.
|
||||
|
||||
@ -109,3 +138,25 @@ The Expired Quotes list provides a summary of all quotes that have already passe
|
||||
- **Balance Due**: The amount of the quote
|
||||
|
||||
.. TIP:: In addition to displaying a helpful overview of your invoicing activity, the Dashboard page is rich in clickable links, providing you with a shortcut to relevant pages you may wish to view. For example, all invoice numbers are clickable, taking you directly to the specific invoice page, and all client names are clickable, taking you directly to the specific client summary page.
|
||||
|
||||
History Sidebar
|
||||
"""""""""""""""
|
||||
|
||||
At the right hand side of your Dashboard screen you'll find the History sidebar, which displays all your recent invoices in a chronological list according to invoice number, together with the name of the client.
|
||||
|
||||
.. TIP:: You can create a new invoice for a particular client by hovering on the right hand side of the tab featuring the client's name. It's just another way to create a new invoice, fast.
|
||||
|
||||
Hide/Show History Sidebar
|
||||
*************************
|
||||
|
||||
You can choose to hide or show the history sidebar at any time by clicking on the Toggle History button, located at the top right corner of the screen. The Toggle History button appears as three horizontal lines in the shape of a button. Click once to close the history sidebar; click again to open.
|
||||
|
||||
Need Help? Feel like Sharing? Introducing Quick Links
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
At the bottom left of the sidebar menu, you'll find a few quick links to make your Invoice Ninja experience even better.
|
||||
|
||||
- **Email**: Click on the email icon to contact us.
|
||||
- **Support**: Click on the support icon to visit our Support Forum.
|
||||
- **Help**: Click on the question mark icon to view Keyboard Shortcuts and Voice Demand libraries.
|
||||
- **Social media**: Click on the social media links to visit our Facebook, Twitter and Github pages.
|
||||
|
@ -30,7 +30,7 @@ The second field is the API_SECRET, enter in the API_SECRET you used in your .en
|
||||
|
||||
Click SAVE.
|
||||
|
||||
You should now be able to login!
|
||||
You should now be able to login with your username and password!
|
||||
|
||||
|
||||
FAQ:
|
||||
|
@ -5,6 +5,8 @@ Update
|
||||
|
||||
To update the app you just need to copy over the latest code. The app tracks the current version in a file called version.txt, if it notices a change it loads ``/update`` to run the database migrations.
|
||||
|
||||
https://download.invoiceninja.com
|
||||
|
||||
.. TIP:: You can use this `shell script <https://pastebin.com/j657uv9A>`_ to automate the update process, consider running it as a daily cron to automatically keep your app up to date.
|
||||
|
||||
If you're moving servers make sure to copy over the .env file.
|
||||
|
@ -21,3 +21,8 @@
|
||||
# you have to enable the following line:
|
||||
# RewriteBase /
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
# Blocks Search Engine Indexing
|
||||
Header set X-Robots-Tag "noindex, nofollow"
|
||||
</IfModule>
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
public/js/daterangepicker.min.js
vendored
4
public/js/daterangepicker.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
public/logo/.htaccess
Normal file
3
public/logo/.htaccess
Normal file
@ -0,0 +1,3 @@
|
||||
<Files *.php>
|
||||
deny from all
|
||||
</Files>
|
@ -1,2 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Disallow: /
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user