Merge branch 'v5-develop' into acss

This commit is contained in:
Benjamin Beganović 2021-10-21 14:51:27 +02:00 committed by GitHub
commit 573c82ed95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 207233 additions and 205959 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

@ -149,11 +149,11 @@ class PaymentMethodController extends Controller
private function getClientGateway()
{
if (request()->query('method') == GatewayType::CREDIT_CARD) {
return $gateway = auth()->user()->client->getCreditCardGateway();
return auth()->user()->client->getCreditCardGateway();
}
if (request()->query('method') == GatewayType::BANK_TRANSFER) {
return $gateway = auth()->user()->client->getBankTransferGateway();
if (in_array(request()->query('method'), [GatewayType::BANK_TRANSFER, GatewayType::DIRECT_DEBIT, GatewayType::SEPA])) {
return auth()->user()->client->getBankTransferGateway();
}
abort(404, 'Gateway not found.');

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,30 @@ 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) {
$cg = CompanyGateway::find($pm['company_gateway_id']);
if ($cg && $cg->fees_and_limits->{GatewayType::DIRECT_DEBIT}->is_enabled) {
return $cg;
}
}
}
}
return null;
}
@ -532,6 +556,10 @@ class Client extends BaseModel implements HasLocalePreference
if ($this->currency()->code == 'EUR') {
return GatewayType::SEPA;
}
if ($this->currency()->code == 'GBP') {
return GatewayType::DIRECT_DEBIT;
}
}
public function getCurrencyCode()
@ -717,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,7 +155,9 @@ class Gateway extends StaticModel
break;
case 52:
return [
GatewayType::BANK_TRANSFER => ['refund' => true, '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

@ -34,6 +34,7 @@ class GatewayType extends StaticModel
const GIROPAY = 15;
const PRZELEWY24 = 16;
const EPS = 17;
const DIRECT_DEBIT = 18;
const ACSS = 19;
const BECS = 20;
@ -86,6 +87,8 @@ class GatewayType extends StaticModel
return ctrans('tets.becs');
case self::ACSS:
return ctrans('texts.acss');
case self::DIRECT_DEBIT:
return ctrans('texts.payment_type_direct_debit');
default:
return 'Undefined.';
break;

View File

@ -50,7 +50,8 @@ class PaymentType extends StaticModel
const GIROPAY = 39;
const PRZELEWY24 = 40;
const EPS = 41;
const BECS = 43;
const DIRECT_DEBIT = 42;
` const BECS = 43;
const ACSS = 44;
public static function parseCardType($cardName)

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

@ -56,6 +56,7 @@ class ACH implements MethodInterface
try {
$redirect = $this->go_cardless->gateway->redirectFlows()->create([
"params" => [
"scheme" => "ach",
"session_token" => $session_token,
"success_redirect_url" => route('client.payment_methods.confirm', [
'method' => GatewayType::BANK_TRANSFER,

View File

@ -0,0 +1,248 @@
<?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\Mail\PaymentFailureMailer;
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 Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\View\View;
class DirectDebit 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 Direct Debit.
*
* @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' => [
'session_token' => $session_token,
'success_redirect_url' => route('client.payment_methods.confirm', [
'method' => GatewayType::DIRECT_DEBIT,
'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.
*
* @param Exception $exception
* @throws PaymentFailed
* @return void
*/
public function processUnsuccessfulAuthorization(\Exception $exception): void
{
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 Direct Debit.
*
* @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.payment_type_direct_debit');
$payment_meta->type = GatewayType::DIRECT_DEBIT;
$payment_meta->state = 'authorized';
$data = [
'payment_meta' => $payment_meta,
'token' => $redirect_flow->links->mandate,
'payment_method_id' => GatewayType::DIRECT_DEBIT,
];
$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 Direct Debit.
*
* @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.direct_debit.pay', $data);
}
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::DIRECT_DEBIT,
'amount' => $this->go_cardless->payment_hash->data->amount_with_fee,
'transaction_reference' => $payment->id,
'gateway_type_id' => GatewayType::DIRECT_DEBIT,
];
$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)
{
PaymentFailureMailer::dispatch($this->go_cardless->client, $payment->status, $this->go_cardless->client->company, $this->go_cardless->payment_hash->data->amount_with_fee);
PaymentFailureMailer::dispatch(
$this->go_cardless->client,
$payment,
$this->go_cardless->client->company,
$payment->amount
);
$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

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

@ -37,6 +37,8 @@ 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;
@ -62,6 +64,18 @@ class GoCardlessPaymentDriver extends BaseDriver
$types[] = GatewayType::BANK_TRANSFER;
}
if (
$this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['GBR'])
) {
$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,28 @@
<?php
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddDirectDebitToPaymentTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('payment_types', function (Blueprint $table) {
$type = new PaymentType();
$type->id = 42;
$type->name = 'Direct Debit';
$type->gateway_type_id = GatewayType::DIRECT_DEBIT;
$type->save();
});
}
}

View File

@ -0,0 +1,23 @@
<?php
use App\Models\GatewayType;
use Illuminate\Database\Migrations\Migration;
class AddGatewayTypeForDirectDebit extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$type = new GatewayType();
$type->id = 18;
$type->alias = 'direct_debit';
$type->name = 'Direct Debit';
$type->save();
}
}

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": "a6d23c92fdbd05308abe92bd317d56eb",
"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": "de3a4a0a08d96f1bc8db5ab84333c49f",
"main.dart.js": "4b982138f175597df211146084e39b66",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"/": "3f4e5212b63fa2b34367df9b913aaebb"
"/": "3debb9cbb687369f006a776cc7ab525b"
};
// The application shell files that are downloaded before a service worker can

19048
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

19030
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

18550
public/main.html.dart.js vendored

File diff suppressed because one or more lines are too long

354740
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.61","build_number":"61"}
{"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

@ -4330,6 +4330,7 @@ $LANG = array(
'becs' => 'BECS Direct Debit',
'becs_mandate' => 'By providing your bank account details, you agree to this <a href="https://stripe.com/au-becs-dd-service-agreement/legal">Direct Debit Request and the Direct Debit Request service agreement</a>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.',
'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.',
'direct_debit' => 'Direct Debit',
'clone_to_expense' => 'Clone to expense',
'checkout' => 'Checkout',
'acss' => 'Pre-authorized debit payments',

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' => 'Direct Debit', 'card_title' => 'Direct Debit'])
@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_direct_debit') }}
(#{{ $token->hashed_id }})</span>
</label>
@endforeach
@endisset
@endcomponent
@else
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'Direct Debit', '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

@ -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 DirectDebitTest 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('Direct Debit')
->assertSee('To pay with a bank account, first you have to add it as payment method.');
});
}
}

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