Merge remote-tracking branch 'upstream/v5-develop' into gocardless-direct-debit

This commit is contained in:
Benjamin Beganović 2021-10-21 14:40:45 +02:00
commit 56761b7585
42 changed files with 645031 additions and 406628 deletions

View File

@ -0,0 +1,91 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Libraries\MultiDB;
use App\Models\Backup;
use App\Models\Design;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use stdClass;
class BackupUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:backup-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Shift backups from DB to storage';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//always return state to first DB
$current_db = config('database.default');
if (! config('ninja.db.multi_db_enabled')) {
$this->handleOnDb();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->handleOnDb();
}
MultiDB::setDB($current_db);
}
}
private function handleOnDb()
{
Backup::whereHas('activity')->whereNotNull('html_backup')->cursor()->each(function($backup){
if($backup->activity->client()->exists()){
$client = $backup->activity->client;
$backup->storeRemotely($backup->html_backup, $client);
}
});
}
}

View File

@ -126,15 +126,13 @@ class CompanySettings extends BaseSettings
public $auto_bill = 'off'; //off,always,optin,optout //@implemented
public $auto_bill_date = 'on_due_date'; // on_due_date , on_send_date //@implemented
//public $design = 'views/pdf/design1.blade.php'; //@deprecated - never used
public $invoice_terms = ''; //@implemented
public $quote_terms = ''; //@implemented
public $invoice_taxes = 0; // ? used in AP only?
// public $enabled_item_tax_rates = 0;
public $invoice_design_id = 'VolejRejNm'; //@implemented
public $quote_design_id = 'VolejRejNm'; //@implemented
public $credit_design_id = 'VolejRejNm'; //@implemented
public $invoice_design_id = 'Wpmbk5ezJn'; //@implemented
public $quote_design_id = 'Wpmbk5ezJn'; //@implemented
public $credit_design_id = 'Wpmbk5ezJn'; //@implemented
public $invoice_footer = ''; //@implemented
public $credit_footer = ''; //@implemented
public $credit_terms = ''; //@implemented
@ -146,7 +144,6 @@ class CompanySettings extends BaseSettings
public $tax_name3 = ''; //@TODO where do we use this?
public $tax_rate3 = 0; //@TODO where do we use this?
public $payment_type_id = '0'; //@TODO where do we use this?
// public $invoice_fields = ''; //@TODO is this redundant, we store this in the custom_fields on the company?
public $valid_until = ''; //@implemented

View File

@ -20,6 +20,7 @@ use App\Utils\Traits\Pdf\PdfMaker;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use stdClass;
@ -136,19 +137,33 @@ class ActivityController extends BaseController
public function downloadHistoricalEntity(DownloadHistoricalEntityRequest $request, Activity $activity)
{
$backup = $activity->backup;
$html_backup = '';
if (! $backup || ! $backup->html_backup) {
/* Refactor 20-10-2021
*
* We have moved the backups out of the database and into object storage.
* In order to handle edge cases, we still check for the database backup
* in case the file no longer exists
*/
if($backup && $backup->filename && Storage::disk(config('filesystems.default'))->exists($backup->filename)){ //disk
$html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->path($backup->filename));
}
elseif($backup && $backup->html_backup){ //db
$html_backup = $backup->html_backup;
}
elseif (! $backup || ! $backup->html_backup) { //failed
return response()->json(['message'=> ctrans('texts.no_backup_exists'), 'errors' => new stdClass], 404);
}
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom)->convertHtmlToPdf($backup->html_backup);
$pdf = (new Phantom)->convertHtmlToPdf($html_backup);
}
elseif(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($backup->html_backup);
$pdf = (new NinjaPdf())->build($html_backup);
}
else {
$pdf = $this->makePdf(null, null, $backup->html_backup);
$pdf = $this->makePdf(null, null, $html_backup);
}
if (isset($activity->invoice_id)) {

View File

@ -738,6 +738,10 @@ class BaseController extends Controller
return redirect()->secure(request()->getRequestUri());
}
/* Clean up URLs and remove query parameters from the URL*/
if(request()->has('login') && request()->input('login') == 'true')
return redirect('/')->with(['login' => "true"]);
$data = [];
//pass report errors bool to front end
@ -748,6 +752,9 @@ class BaseController extends Controller
$data['build'] = request()->has('build') ? request()->input('build') : '';
$data['login'] = request()->has('login') ? request()->input('login') : "false";
if(request()->session()->has('login'))
$data['login'] = "true";
$data['user_agent'] = request()->server('HTTP_USER_AGENT');
$data['path'] = $this->setBuild();

View File

@ -152,7 +152,7 @@ class PaymentMethodController extends Controller
return auth()->user()->client->getCreditCardGateway();
}
if (in_array(request()->query('method'), [GatewayType::BANK_TRANSFER, GatewayType::DIRECT_DEBIT])) {
if (in_array(request()->query('method'), [GatewayType::BANK_TRANSFER, GatewayType::DIRECT_DEBIT, GatewayType::SEPA])) {
return auth()->user()->client->getBankTransferGateway();
}

View File

@ -18,6 +18,8 @@ use App\Http\Requests\GroupSetting\EditGroupSettingRequest;
use App\Http\Requests\GroupSetting\ShowGroupSettingRequest;
use App\Http\Requests\GroupSetting\StoreGroupSettingRequest;
use App\Http\Requests\GroupSetting\UpdateGroupSettingRequest;
use App\Http\Requests\GroupSetting\UploadGroupSettingRequest;
use App\Models\Account;
use App\Models\GroupSetting;
use App\Repositories\GroupSettingRepository;
use App\Transformers\GroupSettingTransformer;
@ -497,4 +499,68 @@ class GroupSettingController extends BaseController
return $this->listResponse(GroupSetting::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
}
/**
* Update the specified resource in storage.
*
* @param UploadGroupSettingRequest $request
* @param GroupSetting $group_setting
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/group_settings/{id}/upload",
* operationId="uploadGroupSetting",
* tags={"group_settings"},
* summary="Uploads a document to a group setting",
* description="Handles the uploading of a document to a group setting",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Group Setting Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Group Setting object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Invoice"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadGroupSettingRequest $request, GroupSetting $group_setting)
{
if(!$this->checkFeature(Account::FEATURE_DOCUMENTS))
return $this->featureFailure();
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $group_setting);
return $this->itemResponse($group_setting->fresh());
}
}

View File

@ -180,6 +180,25 @@ class MigrationController extends BaseController
$company->vendors()->forceDelete();
$company->expenses()->forceDelete();
$settings = $company->settings;
/* Reset all counters to 1 after a purge */
$settings->recurring_invoice_number_counter = 1;
$settings->invoice_number_counter = 1;
$settings->quote_number_counter = 1;
$settings->client_number_counter = 1;
$settings->credit_number_counter = 1;
$settings->task_number_counter = 1;
$settings->expense_number_counter = 1;
$settings->recurring_expense_number_counter = 1;
$settings->recurring_quote_number_counter = 1;
$settings->vendor_number_counter = 1;
$settings->ticket_number_counter = 1;
$settings->payment_number_counter = 1;
$settings->project_number_counter = 1;
$company->settings = $settings;
$company->save();
return response()->json(['message' => 'Settings preserved'], 200);

View File

@ -0,0 +1,47 @@
<?php
/**
* @OA\Schema(
* schema="RecurringExpense",
* type="object",
* @OA\Property(property="id", type="string", example="Opnel5aKBz", description="_________"),
* @OA\Property(property="user_id", type="string", example="", description="__________"),
* @OA\Property(property="assigned_user_id", type="string", example="", description="__________"),
* @OA\Property(property="company_id", type="string", example="", description="________"),
* @OA\Property(property="client_id", type="string", example="", description="________"),
* @OA\Property(property="invoice_id", type="string", example="", description="________"),
* @OA\Property(property="bank_id", type="string", example="", description="________"),
* @OA\Property(property="invoice_currency_id", type="string", example="", description="________"),
* @OA\Property(property="expense_currency_id", type="string", example="", description="________"),
* @OA\Property(property="invoice_category_id", type="string", example="", description="________"),
* @OA\Property(property="payment_type_id", type="string", example="", description="________"),
* @OA\Property(property="recurring_expense_id", type="string", example="", description="________"),
* @OA\Property(property="private_notes", type="string", example="", description="________"),
* @OA\Property(property="public_notes", type="string", example="", description="________"),
* @OA\Property(property="transaction_reference", type="string", example="", description="________"),
* @OA\Property(property="transcation_id", type="string", example="", description="________"),
* @OA\Property(property="custom_value1", type="string", example="", description="________"),
* @OA\Property(property="custom_value2", type="string", example="", description="________"),
* @OA\Property(property="custom_value3", type="string", example="", description="________"),
* @OA\Property(property="custom_value4", type="string", example="", description="________"),
* @OA\Property(property="tax_name1", type="string", example="", description="________"),
* @OA\Property(property="tax_name2", type="string", example="", description="________"),
* @OA\Property(property="tax_rate1", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="tax_rate2", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="tax_name3", type="string", example="", description="________"),
* @OA\Property(property="tax_rate3", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="amount", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="frequency_id", type="number", format="int", example="1", description="_________"),
* @OA\Property(property="remaining_cycles", type="number", format="int", example="1", description="_________"),
* @OA\Property(property="foreign_amount", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="exchange_rate", type="number", format="float", example="0.80", description="_________"),
* @OA\Property(property="date", type="string", example="", description="________"),
* @OA\Property(property="payment_date", type="string", example="", description="________"),
* @OA\Property(property="should_be_invoiced", type="boolean", example=true, description="_________"),
* @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"),
* @OA\Property(property="last_sent_date", type="string", format="date", example="1994-07-30", description="The Date it was sent last"),
* @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The next send date"),
* @OA\Property(property="invoice_documents", type="boolean", example=true, description=""),
* @OA\Property(property="updated_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* @OA\Property(property="archived_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* )
*/

View File

@ -10,8 +10,8 @@
* email="contact@invoiceninja.com"
* ),
* @OA\License(
* name="Attribution Assurance License",
* url="https://opensource.org/licenses/AAL"
* name="Elastic License",
* url="https://www.elastic.co/licensing/elastic-license"
* ),
* ),
* @OA\Server(

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\GroupSetting;
use App\Http\Requests\Request;
class UploadGroupSettingRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->group_setting);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -70,6 +70,13 @@ class StoreInvoiceRequest extends Request
$input['amount'] = 0;
$input['balance'] = 0;
if(array_key_exists('tax_rate1', $input) && is_null($input['tax_rate1']))
$input['tax_rate1'] = 0;
if(array_key_exists('tax_rate2', $input) && is_null($input['tax_rate2']))
$input['tax_rate2'] = 0;
if(array_key_exists('tax_rate3', $input) && is_null($input['tax_rate3']))
$input['tax_rate3'] = 0;
$this->replace($input);
}
}

View File

@ -48,7 +48,7 @@ class StoreUserRequest extends Request
}
if (Ninja::isHosted()) {
$rules['hosted_users'] = new CanAddUserRule(auth()->user()->company()->account);
$rules['id'] = new CanAddUserRule();
}
return $rules;

View File

@ -18,11 +18,9 @@ use Illuminate\Contracts\Validation\Rule;
*/
class CanAddUserRule implements Rule
{
public $account;
public function __construct($account)
public function __construct()
{
$this->account = $account;
}
/**
@ -32,7 +30,7 @@ class CanAddUserRule implements Rule
*/
public function passes($attribute, $value)
{
return $this->account->users->count() < $this->account->num_users;
return auth()->user()->company()->account->users->count() < auth()->user()->company()->account->num_users;
}
/**
@ -40,6 +38,6 @@ class CanAddUserRule implements Rule
*/
public function message()
{
return ctrans('texts.limit_users', ['limit' => $this->account->num_users]);
return ctrans('texts.limit_users', ['limit' => auth()->user()->company()->account->num_users]);
}
}

View File

@ -887,6 +887,7 @@ class CompanyImport implements ShouldQueue
[
'hashed_id',
'company_id',
'backup',
],
[
['users' => 'user_id'],
@ -1192,6 +1193,10 @@ class CompanyImport implements ShouldQueue
}
if(array_key_exists('deleted_at', $obj_array) && $obj_array['deleted_at'] > 1){
$obj_array['deleted_at'] = now();
}
$activity_invitation_key = false;
if($class == 'App\Models\Activity'){
@ -1270,6 +1275,10 @@ class CompanyImport implements ShouldQueue
}
}
if(array_key_exists('deleted_at', $obj_array) && $obj_array['deleted_at'] > 1){
$obj_array['deleted_at'] = now();
}
/* New to convert product ids from old hashes to new hashes*/
if($class == 'App\Models\Subscription'){
$obj_array['product_ids'] = $this->recordProductIds($obj_array['product_ids']);
@ -1320,6 +1329,10 @@ class CompanyImport implements ShouldQueue
}
}
if(array_key_exists('deleted_at', $obj_array) && $obj_array['deleted_at'] > 1){
$obj_array['deleted_at'] = now();
}
/* New to convert product ids from old hashes to new hashes*/
if($class == 'App\Models\Subscription'){
//$obj_array['product_ids'] = $this->recordProductIds($obj_array['product_ids']);

View File

@ -110,6 +110,16 @@ class SendRecurring implements ShouldQueue
$this->recurring_invoice->save();
/*
if ($this->recurring_invoice->company->pause_recurring_until_paid){
$this->recurring_invoice->service()
->stop();
}
*/
//Admin notification for recurring invoice sent.
if ($invoice->invitations->count() >= 1 ) {
$invoice->entityEmailEvent($invoice->invitations->first(), 'invoice', 'email_template_invoice');

View File

@ -11,6 +11,9 @@
namespace App\Models;
use App\Models\Client;
use Illuminate\Support\Facades\Storage;
class Backup extends BaseModel
{
public function getEntityType()
@ -22,4 +25,26 @@ class Backup extends BaseModel
{
return $this->belongsTo(Activity::class);
}
public function storeRemotely(string $html, Client $client)
{
if(strlen($html) == 0)
return;
$path = $client->backup_path() . "/";
$filename = now()->format('Y_m_d'). "_" . md5(time()) . ".html";
$file_path = $path . $filename;
Storage::disk(config('filesystems.default'))->makeDirectory($path, 0775);
Storage::disk(config('filesystems.default'))->put($file_path, $html);
if(Storage::disk(config('filesystems.default'))->exists($file_path)){
$this->html_backup = '';
$this->filename = $file_path;
$this->save();
}
}
}

View File

@ -518,6 +518,18 @@ class Client extends BaseModel implements HasLocalePreference
}
if ($this->currency()->code == 'EUR' && in_array(GatewayType::SEPA, array_column($pms, 'gateway_type_id'))) {
foreach ($pms as $pm) {
if ($pm['gateway_type_id'] == GatewayType::SEPA) {
$cg = CompanyGateway::find($pm['company_gateway_id']);
if ($cg && $cg->fees_and_limits->{GatewayType::SEPA}->is_enabled) {
return $cg;
}
}
}
}
if ($this->country->iso_3166_3 == 'GBR' && in_array(GatewayType::DIRECT_DEBIT, array_column($pms, 'gateway_type_id'))) {
foreach ($pms as $pm) {
if ($pm['gateway_type_id'] == GatewayType::DIRECT_DEBIT) {
@ -733,6 +745,12 @@ class Client extends BaseModel implements HasLocalePreference
})->first()->locale;
}
public function backup_path()
{
return $this->company->company_key.'/'.$this->client_hash.'/backups';
}
public function invoice_filepath($invitation)
{
$contact_key = $invitation->contact->contact_key;

View File

@ -155,8 +155,9 @@ class Gateway extends StaticModel
break;
case 52:
return [
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']],
GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => true, 'webhooks' => [' ']] // GoCardless
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']], // GoCardless
GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => true, 'webhooks' => [' ']],
GatewayType::SEPA => ['refund' => false, 'token_billing' => true, 'webhooks' => [' ']]
];
break;
case 58:

View File

@ -140,7 +140,8 @@ class PayPal
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_BRAINTREE,
$this->braintree->client
$this->braintree->client,
$this->braintree->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->braintree->encodePrimaryKey($payment->id)]);

View File

@ -0,0 +1,250 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\GoCardless;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\GoCardlessPaymentDriver;
use App\Utils\Traits\MakesHash;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\View\View;
class SEPA implements MethodInterface
{
use MakesHash;
protected GoCardlessPaymentDriver $go_cardless;
public function __construct(GoCardlessPaymentDriver $go_cardless)
{
$this->go_cardless = $go_cardless;
$this->go_cardless->init();
}
/**
* Handle authorization for SEPA.
*
* @param array $data
* @return Redirector|RedirectResponse|void
*/
public function authorizeView(array $data)
{
$session_token = \Illuminate\Support\Str::uuid()->toString();
try {
$redirect = $this->go_cardless->gateway->redirectFlows()->create([
'params' => [
'scheme' => 'sepa_core',
'session_token' => $session_token,
'success_redirect_url' => route('client.payment_methods.confirm', [
'method' => GatewayType::SEPA,
'session_token' => $session_token,
]),
'prefilled_customer' => [
'given_name' => auth('contact')->user()->first_name,
'family_name' => auth('contact')->user()->last_name,
'email' => auth('contact')->user()->email,
'address_line1' => auth('contact')->user()->client->address1,
'city' => auth('contact')->user()->client->city,
'postal_code' => auth('contact')->user()->client->postal_code,
],
],
]);
return redirect(
$redirect->redirect_url
);
} catch (\Exception $exception) {
return $this->processUnsuccessfulAuthorization($exception);
}
}
/**
* Handle unsuccessful authorization for SEPA.
*
* @param Exception $exception
* @return void
*/
public function processUnsuccessfulAuthorization(\Exception $exception): void
{
$this->go_cardless->sendFailureMail($exception->getMessage());
SystemLogger::dispatch(
$exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_GOCARDLESS,
$this->go_cardless->client,
$this->go_cardless->client->company,
);
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
/**
* Handle authorization response for SEPA.
*
* @param Request $request
* @return RedirectResponse|void
*/
public function authorizeResponse(Request $request)
{
try {
$redirect_flow = $this->go_cardless->gateway->redirectFlows()->complete(
$request->redirect_flow_id,
['params' => [
'session_token' => $request->session_token
]],
);
$payment_meta = new \stdClass;
$payment_meta->brand = ctrans('texts.sepa');
$payment_meta->type = GatewayType::SEPA;
$payment_meta->state = 'authorized';
$data = [
'payment_meta' => $payment_meta,
'token' => $redirect_flow->links->mandate,
'payment_method_id' => GatewayType::SEPA,
];
$payment_method = $this->go_cardless->storeGatewayToken($data, ['gateway_customer_reference' => $redirect_flow->links->customer]);
return redirect()->route('client.payment_methods.show', $payment_method->hashed_id);
} catch (\Exception $exception) {
return $this->processUnsuccessfulAuthorization($exception);
}
}
/**
* Payment view for SEPA.
*
* @param array $data
* @return View
*/
public function paymentView(array $data): View
{
$data['gateway'] = $this->go_cardless;
$data['amount'] = $this->go_cardless->convertToGoCardlessAmount($data['total']['amount_with_fee'], $this->go_cardless->client->currency()->precision);
$data['currency'] = $this->go_cardless->client->getCurrencyCode();
return render('gateways.gocardless.sepa.pay', $data);
}
/**
* Handle the payment page for SEPA.
*
* @param PaymentResponseRequest $request
* @return RedirectResponse|App\PaymentDrivers\GoCardless\never|void
*/
public function paymentResponse(PaymentResponseRequest $request)
{
$token = ClientGatewayToken::find(
$this->decodePrimaryKey($request->source)
)->firstOrFail();
try {
$payment = $this->go_cardless->gateway->payments()->create([
'params' => [
'amount' => $request->amount,
'currency' => $request->currency,
'metadata' => [
'payment_hash' => $this->go_cardless->payment_hash->hash,
],
'links' => [
'mandate' => $token->token,
],
],
]);
if ($payment->status === 'pending_submission') {
return $this->processPendingPayment($payment, ['token' => $token->hashed_id]);
}
return $this->processUnsuccessfulPayment($payment);
} catch (\Exception $exception) {
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
}
/**
* Handle pending payments for Direct Debit.
*
* @param ResourcesPayment $payment
* @param array $data
* @return RedirectResponse
*/
public function processPendingPayment(\GoCardlessPro\Resources\Payment $payment, array $data = [])
{
$data = [
'payment_method' => $data['token'],
'payment_type' => PaymentType::SEPA,
'amount' => $this->go_cardless->payment_hash->data->amount_with_fee,
'transaction_reference' => $payment->id,
'gateway_type_id' => GatewayType::SEPA,
];
$payment = $this->go_cardless->createPayment($data, Payment::STATUS_PENDING);
SystemLogger::dispatch(
['response' => $payment, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_GOCARDLESS,
$this->go_cardless->client,
$this->go_cardless->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->go_cardless->encodePrimaryKey($payment->id)]);
}
/**
* Process unsuccessful payments for Direct Debit.
*
* @param ResourcesPayment $payment
* @return never
*/
public function processUnsuccessfulPayment(\GoCardlessPro\Resources\Payment $payment)
{
$this->go_cardless->sendFailureMail(
$payment->status
);
$message = [
'server_response' => $payment,
'data' => $this->go_cardless->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_GOCARDLESS,
$this->go_cardless->client,
$this->go_cardless->client->company,
);
throw new PaymentFailed('Failed to process the payment.', 500);
}
}

View File

@ -38,6 +38,7 @@ class GoCardlessPaymentDriver extends BaseDriver
public static $methods = [
GatewayType::BANK_TRANSFER => \App\PaymentDrivers\GoCardless\ACH::class,
GatewayType::DIRECT_DEBIT => \App\PaymentDrivers\GoCardless\DirectDebit::class,
GatewayType::SEPA => \App\PaymentDrivers\GoCardless\SEPA::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_GOCARDLESS;
@ -71,6 +72,10 @@ class GoCardlessPaymentDriver extends BaseDriver
$types[] = GatewayType::DIRECT_DEBIT;
}
if ($this->client->currency()->code === 'EUR') {
$types[] = GatewayType::SEPA;
}
return $types;
}

View File

@ -73,24 +73,25 @@ class ActivityRepository extends BaseRepository
if ($entity instanceof User || $entity->company->is_disabled)
return;
$backup = new Backup();
if (get_class($entity) == Invoice::class
|| get_class($entity) == Quote::class
|| get_class($entity) == Credit::class
|| get_class($entity) == RecurringInvoice::class
) {
$backup = new Backup();
$entity->load('client');
$contact = $entity->client->primary_contact()->first();
$backup->html_backup = $this->generateHtml($entity);
$backup->amount = $entity->amount;
$backup->activity_id = $activity->id;
$backup->json_backup = '';
$backup->save();
$backup->storeRemotely($this->generateHtml($entity), $entity->client);
}
$backup->activity_id = $activity->id;
$backup->json_backup = '';
$backup->save();
}
public function getTokenId(array $event_vars)
@ -126,7 +127,7 @@ class ActivityRepository extends BaseRepository
if(!$entity->invitations()->exists() || !$design){
nlog("No invitations for entity {$entity->id} - {$entity->number}");
return;
return '';
}
$entity->load('client.company', 'invitations');

View File

@ -483,6 +483,30 @@ class InvoiceService
}
/*
//if paid invoice is attached to a recurring invoice - check if we need to unpause the recurring invoice
if ($this->invoice->status_id == Invoice::STATUS_PAID &&
$this->invoice->recurring_id &&
$this->invoice->company->pause_recurring_until_paid &&
($this->invoice->recurring_invoice->status_id != RecurringInvoice::STATUS_ACTIVE || $this->invoice->recurring_invoice->status_id != RecurringInvoice::STATUS_COMPLETED))
{
$recurring_invoice = $this->invoice->recurring_invoice;
// Check next_send_date if it is in the past - calculate
$next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay();
if(next_send_date->lt(now())){
$recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d'));
$recurring_invoice->save();
}
// Start the recurring invoice
$recurring_invoice->service()
->start();
}
*/
return $this;
}

View File

@ -82,6 +82,9 @@ class MarkPaid extends AbstractService
->updateBalance($payment->amount * -1)
->updatePaidToDate($payment->amount)
->setStatus(Invoice::STATUS_PAID)
->save();
$this->invoice->service()
->applyNumber()
->deletePdf()
->save();
@ -103,7 +106,10 @@ class MarkPaid extends AbstractService
->updatePaidToDate($payment->amount)
->save();
$this->invoice->service()->workFlow()->save();
$this->invoice
->service()
->workFlow()
->save();
return $this->invoice;
}

View File

@ -84,9 +84,19 @@ class SystemHealth
'jobs_pending' => (int) Queue::size(),
'pdf_engine' => (string) self::getPdfEngine(),
'queue' => (string) config('queue.default'),
'trailing_slash' => (bool) self::checkUrlState(),
];
}
public static function checkUrlState()
{
if (env('APP_URL') && substr(env('APP_URL'), -1) == '/')
return true;
return false;
}
public static function getPdfEngine()
{
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja')

View File

@ -13,7 +13,7 @@
"tasks",
"freelancer"
],
"license": "Attribution Assurance License",
"license": "Elastic License",
"authors": [
{
"name": "Hillel Coren",

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddFilenameToBackupsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('backups', function (Blueprint $table) {
$table->text('filename')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@ -3,7 +3,7 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"version.json": "07a43895b172742ab22bb808918b117a",
"version.json": "27abc97e9c76cf112b697fa080c304b5",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"assets/fonts/MaterialIcons-Regular.otf": "4e6447691c9509f7acdbf8a931a85ca1",
@ -32,9 +32,9 @@ const RESOURCES = {
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"main.dart.js": "6d7015b94cf66c9fd426081b7bfb5b97",
"main.dart.js": "4b982138f175597df211146084e39b66",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"/": "4da4247bb7f072054c574b10e7370438"
"/": "3debb9cbb687369f006a776cc7ab525b"
};
// The application shell files that are downloaded before a service worker can

221742
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

223944
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

237180
public/main.html.dart.js vendored Normal file

File diff suppressed because one or more lines are too long

358286
public/main.next.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"app_name":"invoiceninja_flutter","version":"5.0.60","build_number":"60"}
{"app_name":"invoiceninja_flutter","version":"5.0.62","build_number":"62"}

View File

@ -88,12 +88,8 @@ class SquareCreditCard {
}
catch(typeError){
console.log(typeError);
die("failed in the catch");
}
// console.log(" verification tokem = " + verificationToken.token);
// verificationToken = verificationResults.token;
console.debug('Verification Token:', verificationToken);
document.querySelector('input[name="verificationToken"]').value =

View File

@ -36,6 +36,7 @@
class="input w-full"
type="email"
name="{{ $field['key'] }}"
value="{{ old($field['key']) }}"
{{ $field['required'] ? 'required' : '' }} />
@elseif($field['key'] === 'password')
<input
@ -63,6 +64,7 @@
id="{{ $field['key'] }}"
class="input w-full"
name="{{ $field['key'] }}"
value="{{ old($field['key']) }}"
{{ $field['required'] ? 'required' : '' }} />
@endif

View File

@ -39,7 +39,7 @@
data-token="{{ $token->token }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">{{ $token->meta->email }}</span>
<span class="ml-1 cursor-pointer">{{ optional($token->meta)->email }}</span>
</label>
@endforeach
@endisset

View File

@ -0,0 +1,56 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_SEPA'), 'card_title' => ctrans('texts.payment_type_SEPA')])
@section('gateway_content')
@if (count($tokens) > 0)
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@include('portal.ninja2020.gateways.includes.payment_details')
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="source" value="">
<input type="hidden" name="amount" value="{{ $amount }}">
<input type="hidden" name="currency" value="{{ $currency }}">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
</form>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
@if (count($tokens) > 0)
@foreach ($tokens as $token)
<label class="mr-4">
<input type="radio" data-token="{{ $token->hashed_id }}" name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token" />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.payment_type_SEPA') }}
(#{{ $token->hashed_id }})</span>
</label>
@endforeach
@endisset
@endcomponent
@else
@component('portal.ninja2020.components.general.card-element-single', ['title' => ctrans('texts.payment_type_SEPA'), 'show_title' => false])
<span>{{ ctrans('texts.bank_account_not_linked') }}</span>
<a class="button button-link text-primary"
href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
@endcomponent
@endif
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@push('footer')
<script>
Array
.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => element.addEventListener('click', (element) => {
document.querySelector('input[name=source]').value = element.target.dataset.token;
}));
document.getElementById('pay-now').addEventListener('click', function() {
document.getElementById('server-response').submit();
});
</script>
@endpush

View File

@ -86,6 +86,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::resource('group_settings', 'GroupSettingController');
Route::post('group_settings/bulk', 'GroupSettingController@bulk');
Route::put('group_settings/{group_setting}/upload', 'GroupSettingController@upload')->name('group_settings.upload');
Route::post('import', 'ImportController@import')->name('import.import');
Route::post('import_json', 'ImportJsonController@import')->name('import.import_json');

View File

@ -0,0 +1,42 @@
<?php
namespace Tests\Browser\ClientPortal\Gateways\GoCardless;
use App\Models\CompanyGateway;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\ClientPortal\Login;
use Tests\DuskTestCase;
class SEPATest extends DuskTestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (static::$browsers as $browser) {
$browser->driver->manage()->deleteAllCookies();
}
$this->disableCompanyGateways();
CompanyGateway::where('gateway_key', 'b9886f9257f0c6ee7c302f1c74475f6c')->restore();
$this->browse(function (Browser $browser) {
$browser
->visit(new Login())
->auth();
});
}
public function testPayingWithNoPreauthorizedIsntPossible()
{
$this->browse(function (Browser $browser) {
$browser
->visitRoute('client.invoices.index')
->click('@pay-now')
->press('Pay Now')
->clickLink('SEPA Direct Debit')
->assertSee('To pay with a bank account, first you have to add it as payment method.');
});
}
}