1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-21 08:51:34 +02:00

Merge pull request #4172 from turbo124/v5-stable

5.0.18 release
This commit is contained in:
David Bomba 2020-10-16 20:14:26 +11:00 committed by GitHub
commit 89fb681a22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 126148 additions and 121521 deletions

View File

@ -1,10 +1,10 @@
on: on:
push: push:
branches: branches:
- v2 - v5-develop
pull_request: pull_request:
branches: branches:
- v2 - v5-develop
name: phpunit name: phpunit
jobs: jobs:
@ -62,7 +62,7 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
with: with:
ref: v2 ref: v5-develop
fetch-depth: 1 fetch-depth: 1
- name: Copy .env - name: Copy .env

View File

@ -12,7 +12,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v1 uses: actions/checkout@v1
with: with:
ref: v2 ref: v5-stable
- name: Copy .env file - name: Copy .env file
run: | run: |

View File

@ -1 +1 @@
5.0.17 5.0.18

View File

@ -101,7 +101,7 @@ class CreateSingleAccount extends Command
$this->warmCache(); $this->warmCache();
$this->createSmallAccount(); $this->createSmallAccount();
} }
private function createSmallAccount() private function createSmallAccount()
@ -176,8 +176,8 @@ class CreateSingleAccount extends Command
$client = $company->clients->random(); $client = $company->clients->random();
$this->info('creating credit for client #'.$client->id); // $this->info('creating credit for client #'.$client->id);
$this->createCredit($client); // $this->createCredit($client); /** Prevents Stripe from running payments. */
$client = $company->clients->random(); $client = $company->clients->random();
@ -497,7 +497,7 @@ class CreateSingleAccount extends Command
} }
private function createGateways($company, $user) private function createGateways($company, $user)
{ {
if (config('ninja.testvars.stripe') && ($this->gateway == 'all' || $this->gateway == 'stripe')) { if (config('ninja.testvars.stripe') && ($this->gateway == 'all' || $this->gateway == 'stripe')) {
$cg = new CompanyGateway; $cg = new CompanyGateway;

View File

@ -154,10 +154,12 @@ class CompanySettings extends BaseSettings
public $email_style_custom = ''; //the template itself public $email_style_custom = ''; //the template itself
public $email_subject_invoice = ''; public $email_subject_invoice = '';
public $email_subject_quote = ''; public $email_subject_quote = '';
public $email_subject_credit = '';
public $email_subject_payment = ''; public $email_subject_payment = '';
public $email_subject_payment_partial = ''; public $email_subject_payment_partial = '';
public $email_subject_statement = ''; public $email_subject_statement = '';
public $email_template_invoice = ''; public $email_template_invoice = '';
public $email_template_credit = '';
public $email_template_quote = ''; public $email_template_quote = '';
public $email_template_payment = ''; public $email_template_payment = '';
public $email_template_payment_partial = ''; public $email_template_payment_partial = '';
@ -350,10 +352,12 @@ class CompanySettings extends BaseSettings
'email_signature' => 'string', 'email_signature' => 'string',
'email_subject_invoice' => 'string', 'email_subject_invoice' => 'string',
'email_subject_quote' => 'string', 'email_subject_quote' => 'string',
'email_subject_credit' => 'string',
'email_subject_payment' => 'string', 'email_subject_payment' => 'string',
'email_subject_payment_partial' => 'string', 'email_subject_payment_partial' => 'string',
'email_template_invoice' => 'string', 'email_template_invoice' => 'string',
'email_template_quote' => 'string', 'email_template_quote' => 'string',
'email_template_credit' => 'string',
'email_template_payment' => 'string', 'email_template_payment' => 'string',
'email_template_payment_partial' => 'string', 'email_template_payment_partial' => 'string',
'email_subject_reminder1' => 'string', 'email_subject_reminder1' => 'string',

View File

@ -30,6 +30,9 @@ class EmailTemplateDefaults
case 'email_template_quote': case 'email_template_quote':
return self::emailQuoteTemplate(); return self::emailQuoteTemplate();
break; break;
case 'email_template_credit':
return self::emailCreditTemplate();
break;
case 'email_template_payment': case 'email_template_payment':
return self::emailPaymentTemplate(); return self::emailPaymentTemplate();
break; break;
@ -69,6 +72,9 @@ class EmailTemplateDefaults
case 'email_subject_quote': case 'email_subject_quote':
return self::emailQuoteSubject(); return self::emailQuoteSubject();
break; break;
case 'email_subject_credit':
return self::emailCreditSubject();
break;
case 'email_subject_payment': case 'email_subject_payment':
return self::emailPaymentSubject(); return self::emailPaymentSubject();
break; break;
@ -109,7 +115,11 @@ class EmailTemplateDefaults
public static function emailInvoiceSubject() public static function emailInvoiceSubject()
{ {
return ctrans('texts.invoice_subject', ['number'=>'$number', 'account'=>'$company.name']); return ctrans('texts.invoice_subject', ['number'=>'$number', 'account'=>'$company.name']);
//return Parsedown::instance()->line(self::transformText('invoice_subject')); }
public static function emailCreditSubject()
{
return ctrans('texts.credit_subject', ['number'=>'$number', 'account'=>'$company.name']);
} }
public static function emailInvoiceTemplate() public static function emailInvoiceTemplate()
@ -122,14 +132,11 @@ class EmailTemplateDefaults
$invoice_message = '<p>'.self::transformText('invoice_message').'</p><br><br><p>$view_link</p>'; $invoice_message = '<p>'.self::transformText('invoice_message').'</p><br><br><p>$view_link</p>';
return $invoice_message; return $invoice_message;
//return $converter->convertToHtml($invoice_message);
} }
public static function emailQuoteSubject() public static function emailQuoteSubject()
{ {
return ctrans('texts.quote_subject', ['number'=>'$number', 'account'=>'$company.name']); return ctrans('texts.quote_subject', ['number'=>'$number', 'account'=>'$company.name']);
//return Parsedown::instance()->line(self::transformText('quote_subject'));
} }
public static function emailQuoteTemplate() public static function emailQuoteTemplate()
@ -158,6 +165,17 @@ class EmailTemplateDefaults
} }
public static function emailCreditTemplate()
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
return $converter->convertToHtml(self::transformText('credit_message'));
}
public static function emailPaymentPartialTemplate() public static function emailPaymentPartialTemplate()
{ {
$converter = new CommonMarkConverter([ $converter = new CommonMarkConverter([

View File

@ -94,15 +94,15 @@ class InvoiceItemFactory
$item->cost = $faker->randomFloat(2, -1, -1000); $item->cost = $faker->randomFloat(2, -1, -1000);
$item->line_total = $item->quantity * $item->cost; $item->line_total = $item->quantity * $item->cost;
$item->is_amount_discount = true; $item->is_amount_discount = true;
$item->discount = $faker->numberBetween(1, 10); $item->discount = 0;
$item->notes = $faker->realText(20); $item->notes = $faker->realText(20);
$item->product_key = $faker->word(); $item->product_key = $faker->word();
$item->custom_value1 = $faker->realText(10); $item->custom_value1 = $faker->realText(10);
$item->custom_value2 = $faker->realText(10); $item->custom_value2 = $faker->realText(10);
$item->custom_value3 = $faker->realText(10); $item->custom_value3 = $faker->realText(10);
$item->custom_value4 = $faker->realText(10); $item->custom_value4 = $faker->realText(10);
$item->tax_name1 = 'GST'; $item->tax_name1 = '';
$item->tax_rate1 = 10.00; $item->tax_rate1 = 0;
$item->type_id = "1"; $item->type_id = "1";
$data[] = $item; $data[] = $item;

View File

@ -288,5 +288,39 @@ class InvoiceSum
return $this->getTotalTaxes(); return $this->getTotalTaxes();
} }
public function purgeTaxes()
{
$this->tax_rate1 = 0;
$this->tax_name1 = '';
$this->tax_rate2 = 0;
$this->tax_name2 = '';
$this->tax_rate3 = 0;
$this->tax_name3 = '';
$this->discount = 0;
$line_items = collect($this->invoice->line_items);
$items = $line_items->map(function ($item){
$item->tax_rate1 = 0;
$item->tax_rate2 = 0;
$item->tax_rate3 = 0;
$item->tax_name1 = '';
$item->tax_name2 = '';
$item->tax_name3 = '';
$item->discount = 0;
return $item;
});
$this->invoice->line_items = $items->toArray();
$this->build();
return $this;
}
} }

View File

@ -302,4 +302,9 @@ class InvoiceSumInclusive
return $this->getTotalTaxes(); return $this->getTotalTaxes();
} }
public function purgeTaxes()
{
return $this;
}
} }

View File

@ -17,6 +17,7 @@ use App\Events\Misc\InvitationWasViewed;
use App\Events\Quote\QuoteWasViewed; use App\Events\Quote\QuoteWasViewed;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Invoice\UniqueInvoiceNumberRule; use App\Http\ValidationRules\Invoice\UniqueInvoiceNumberRule;
use App\Http\ValidationRules\Project\ValidProjectForClient;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Invoice; use App\Models\Invoice;
use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\CleanLineItems;
@ -47,12 +48,14 @@ class StoreInvoiceRequest extends Request
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
} }
$rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id;
$rules['invitations.*.client_contact_id'] = 'distinct'; $rules['invitations.*.client_contact_id'] = 'distinct';
$rules['number'] = new UniqueInvoiceNumberRule($this->all()); $rules['number'] = new UniqueInvoiceNumberRule($this->all());
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
return $rules; return $rules;
} }
@ -68,6 +71,10 @@ class StoreInvoiceRequest extends Request
$input['client_id'] = $this->decodePrimaryKey($input['client_id']); $input['client_id'] = $this->decodePrimaryKey($input['client_id']);
} }
if (array_key_exists('project_id', $input) && is_string($input['project_id'])) {
$input['project_id'] = $this->decodePrimaryKey($input['project_id']);
}
if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) {
$input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
} }

View File

@ -33,7 +33,8 @@ class StoreProjectRequest extends Request
{ {
$rules = []; $rules = [];
$rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId(); //$rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId();
$rules['name'] = 'required';
$rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id;
return $rules; return $rules;

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\RecurringInvoice; namespace App\Http\Requests\RecurringInvoice;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Recurring\UniqueRecurringInvoiceNumberRule;
use App\Models\Client; use App\Models\Client;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\CleanLineItems;
@ -52,6 +53,8 @@ class StoreRecurringInvoiceRequest extends Request
$rules['frequency_id'] = 'required|integer'; $rules['frequency_id'] = 'required|integer';
$rules['number'] = new UniqueRecurringInvoiceNumberRule($this->all());
return $rules; return $rules;
} }

View File

@ -48,6 +48,10 @@ class UpdateRecurringInvoiceRequest extends Request
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
} }
if ($this->input('number')) {
$rules['number'] = 'unique:recurring_invoices,number,'.$this->id.',id,company_id,'.$this->recurring_invoice->company_id;
}
return $rules; return $rules;
} }

View File

@ -48,10 +48,13 @@ class StoreVendorRequest extends Request
protected function prepareForValidation() protected function prepareForValidation()
{ {
// $input = $this->all(); $input = $this->all();
if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) {
// $this->replace($input); $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
}
$this->replace($input);
} }
public function messages() public function messages()

View File

@ -69,6 +69,10 @@ class UpdateVendorRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) {
$input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
}
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -0,0 +1,55 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\ValidationRules\Project;
use App\Models\Project;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Validation\Rule;
/**
* Class ValidProjectForClient.
*/
class ValidProjectForClient implements Rule
{
use MakesHash;
public $input;
public function __construct($input)
{
$this->input = $input;
}
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
if(is_string($this->input['project_id']))
$this->input['project_id'] = $this->decodePrimaryKey($this->input['project_id']);
$project = Project::findOrFail($this->input['project_id']);
return $project->client_id == $this->input['client_id'];
}
/**
* @return string
*/
public function message()
{
return "Project client does not match entity client";
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\ValidationRules\Recurring;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Models\User;
use Illuminate\Contracts\Validation\Rule;
/**
* Class UniqueRecurringInvoiceNumberRule.
*/
class UniqueRecurringInvoiceNumberRule implements Rule
{
public $input;
public function __construct($input)
{
$this->input = $input;
}
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return $this->checkIfInvoiceNumberUnique(); //if it exists, return false!
}
/**
* @return string
*/
public function message()
{
return "Recurring Invoice number {$this->input['number']} already taken";
}
/**
* @param $email
*
* //off,when_sent,when_paid
*
* @return bool
*/
private function checkIfInvoiceNumberUnique() : bool
{
if(empty($this->input['number']))
return true;
$invoice = RecurringInvoice::where('client_id', $this->input['client_id'])
->where('number', $this->input['number'])
->withTrashed()
->exists();
if ($invoice) {
return false;
}
return true;
}
}

View File

@ -318,7 +318,7 @@ class Company extends BaseModel
return isset($this->settings->language_id) && $this->language() ? $this->language()->locale : config('ninja.i18n.locale'); return isset($this->settings->language_id) && $this->language() ? $this->language()->locale : config('ninja.i18n.locale');
} }
public function getLogo() public function getLogo() :?string
{ {
return $this->settings->company_logo ?: null; return $this->settings->company_logo ?: null;
} }
@ -394,7 +394,6 @@ class Company extends BaseModel
public function company_users() public function company_users()
{ {
//return $this->hasMany(CompanyUser::class)->withTimestamps();
return $this->hasMany(CompanyUser::class); return $this->hasMany(CompanyUser::class);
} }

View File

@ -50,7 +50,7 @@ class RecurringInvoiceInvitation extends BaseModel
*/ */
public function contact() public function contact()
{ {
return $this->belongsTo(ClientContact::class)->withTrashed(); return $this->belongsTo(ClientContact::class, 'client_contact_id', 'id')->withTrashed();
} }
/** /**

View File

@ -33,7 +33,10 @@ class RecurringInvoiceRepository extends BaseRepository
$invoice_calc = new InvoiceSum($invoice, $invoice->settings); $invoice_calc = new InvoiceSum($invoice, $invoice->settings);
$invoice->service()->applyNumber()->save(); $invoice->service()
->applyNumber()
->createInvitations()
->save();
$invoice = $invoice_calc->build()->getInvoice(); $invoice = $invoice_calc->build()->getInvoice();

View File

@ -57,6 +57,7 @@ class VendorRepository extends BaseRepository
*/ */
public function save(array $data, Vendor $vendor) : ?Vendor public function save(array $data, Vendor $vendor) : ?Vendor
{ {
$vendor->fill($data); $vendor->fill($data);
$vendor->save(); $vendor->save();

View File

@ -91,7 +91,7 @@ class HandleReversal extends AbstractService
$credit_calc = new InvoiceSum($credit); $credit_calc = new InvoiceSum($credit);
$credit_calc->build(); $credit_calc->build();
$credit = $credit_calc->getCredit(); $credit = $credit_calc->purgeTaxes()->getCredit();
$credit->service()->markSent()->save(); $credit->service()->markSent()->save();
} }

View File

@ -28,7 +28,7 @@ class CompanyLedgerTransformer extends EntityTransformer
*/ */
public function transform(CompanyLedger $company_ledger) public function transform(CompanyLedger $company_ledger)
{ {
$entity_name = lcfirst(class_basename($company_ledger->company_ledgerable_type)).'_id'; $entity_name = lcfirst(rtrim(class_basename($company_ledger->company_ledgerable_type), 's')).'_id';
return [ return [
$entity_name => (string) $this->encodePrimaryKey($company_ledger->company_ledgerable_id), $entity_name => (string) $this->encodePrimaryKey($company_ledger->company_ledgerable_id),

View File

@ -104,6 +104,10 @@ trait AppSetup
'subject' => EmailTemplateDefaults::emailStatementSubject(), 'subject' => EmailTemplateDefaults::emailStatementSubject(),
'body' => EmailTemplateDefaults::emailStatementTemplate(), 'body' => EmailTemplateDefaults::emailStatementTemplate(),
], ],
'credit' => [
'subject' => EmailTemplateDefaults::emailCreditSubject(),
'body' => EmailTemplateDefaults::emailCreditTemplate(),
],
]; ];
Cache::forever($name, $data); Cache::forever($name, $data);

View File

@ -11,6 +11,8 @@
namespace App\Utils\Traits; namespace App\Utils\Traits;
use Illuminate\Support\Str;
/** /**
* Class Inviteable. * Class Inviteable.
*/ */
@ -42,7 +44,9 @@ trait Inviteable
public function getLink() :string public function getLink() :string
{ {
$entity_type = strtolower(class_basename($this->entityType())); //$entity_type = strtolower(class_basename($this->entityType()));
$entity_type = Str::snake(class_basename($this->entityType()));
//$this->with('company','contact',$this->entity_type); //$this->with('company','contact',$this->entity_type);
//$this->with('company'); //$this->with('company');

View File

@ -6,13 +6,13 @@ return [
'license_url' => 'https://app.invoiceninja.com', 'license_url' => 'https://app.invoiceninja.com',
'production' => env('NINJA_PROD', false), 'production' => env('NINJA_PROD', false),
'license' => env('NINJA_LICENSE', ''), 'license' => env('NINJA_LICENSE', ''),
'version_url' => 'https://raw.githubusercontent.com/invoiceninja/invoiceninja/v2/VERSION.txt', 'version_url' => 'https://raw.githubusercontent.com/invoiceninja/invoiceninja/v5-stable/VERSION.txt',
'app_name' => env('APP_NAME'), 'app_name' => env('APP_NAME'),
'app_env' => env('APP_ENV', 'selfhosted'), 'app_env' => env('APP_ENV', 'selfhosted'),
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/').'/', 'app_url' => rtrim(env('APP_URL', ''), '/').'/',
'app_domain' => env('APP_DOMAIN', ''), 'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.0.17', 'app_version' => '5.0.18',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),

View File

@ -1,5 +1,5 @@
{ {
"video": false, "video": false,
"baseUrl": "http://ninja.test:8000/", "baseUrl": "http://localhost:8000/",
"chromeWebSecurity": false "chromeWebSecurity": false
} }

View File

@ -0,0 +1,48 @@
import { second } from '../fixtures/example.json';
describe('Checkout Credit Card Payments', () => {
beforeEach(() => {
// cy.useGateway(second);
cy.clientLogin();
});
it('should be able to complete payment using checkout credit card', () => {
cy.visit('/client/invoices');
cy.get('#unpaid-checkbox').click();
cy.get('[data-cy=pay-now')
.first()
.click();
cy.location('pathname').should('eq', '/client/invoices/payment');
cy.get('[data-cy=payment-methods-dropdown').click();
cy.get('[data-cy=payment-method')
.first()
.click();
cy.wait(8000);
cy.get('.cko-pay-now.show')
.first()
.click();
cy.wait(3000);
cy.getWithinIframe('[data-checkout="card-number"]').type(
'4242424242424242'
);
cy.getWithinIframe('[data-checkout="expiry-month"]').type('12');
cy.getWithinIframe('[data-checkout="expiry-year"]').type('30');
cy.getWithinIframe('[data-checkout="cvv"]').type('100');
cy.getWithinIframe('.form-submit')
.first()
.click();
cy.wait(5000);
cy.url().should('contain', '/client/payments');
});
});

View File

@ -1,5 +1,10 @@
{ {
"name": "Using fixtures to represent data", "name": "Using fixtures to represent data",
"email": "hello@cypress.io", "email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes" "body": "Fixtures are a great way to mock data for responses to routes",
}
"first": "VolejRejNm",
"second": "Wpmbk5ezJn",
"url": "http://localhost:8000"
}

View File

@ -19,7 +19,7 @@ describe('Credits', () => {
.should('contain.text', 'Credits'); .should('contain.text', 'Credits');
}); });
it('should have required table elements', () => { /* it('should have required table elements', () => {
cy.visit('/client/credits'); cy.visit('/client/credits');
cy.get('body') cy.get('body')
@ -33,5 +33,5 @@ describe('Credits', () => {
.should(location => { .should(location => {
expect(location.pathname).to.eq('/client/credits/VolejRejNm'); expect(location.pathname).to.eq('/client/credits/VolejRejNm');
}); });
}); });*/
}); });

View File

@ -19,35 +19,6 @@ context('Payment methods', () => {
.should('contain.text', 'Payment Method'); .should('contain.text', 'Payment Method');
}); });
it('should add stripe credit card', () => {
cy.visit('/client/payment_methods');
cy.get('body')
.find('#add-payment-method')
.first()
.should('contain.text', 'Add Payment Method')
.click()
cy.location().should(location => {
expect(location.pathname).to.eq('/client/payment_methods/create');
});
cy.wait(3000);
cy.get('#cardholder-name').type('Invoice Ninja');
cy.getWithinIframe('[name="cardnumber"]').type('4242424242424242');
cy.getWithinIframe('[name="exp-date"]').type('2442');
cy.getWithinIframe('[name="cvc"]').type('242');
cy.getWithinIframe('[name="postal"]').type('12345');
cy.get('#card-button').click();
cy.location().should(location => {
expect(location.pathname).to.eq('/client/payment_methods');
});
});
it('should have per page options dropdown', () => { it('should have per page options dropdown', () => {
cy.visit('/client/payment_methods'); cy.visit('/client/payment_methods');

View File

@ -27,20 +27,4 @@ context('Payments', () => {
.first() .first()
.should('have.value', '10'); .should('have.value', '10');
}); });
});
it('should have required table elements', () => {
cy.visit('/client/payments');
cy.get('body')
.find('table.payments-table > tbody > tr')
.first()
.find('a')
.first()
.should('contain.text', 'View')
.click()
.location()
.should(location => {
expect(location.pathname).to.eq('/client/payments/VolejRejNm');
});
});
})

View File

@ -3,8 +3,6 @@ context('Recurring invoices', () => {
cy.clientLogin(); cy.clientLogin();
}); });
// test url
it('should show recurring invoices page', () => { it('should show recurring invoices page', () => {
cy.visit('/client/recurring_invoices'); cy.visit('/client/recurring_invoices');
@ -29,20 +27,4 @@ context('Recurring invoices', () => {
.first() .first()
.should('have.value', '10'); .should('have.value', '10');
}); });
it('should have required table elements', () => {
cy.visit('/client/recurring_invoices');
cy.get('body')
.find('table.recurring-invoices-table > tbody > tr')
.first()
.find('a')
.first()
.should('contain.text', 'View')
.click()
.location()
.should(location => {
expect(location.pathname).to.eq('/client/recurring_invoices/VolejRejNm');
});
});
}); });

View File

@ -0,0 +1,47 @@
describe('Stripe Credit Card Payments', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should be able to add credit card using Stripe', () => {
cy.visit('/client/payment_methods');
cy.get('[data-cy=add-payment-method]').click();
cy.get('[data-cy=add-credit-card-link]').click();
cy.get('#cardholder-name').type('Invoice Ninja');
cy.getWithinIframe('[name="cardnumber"]').type('4242424242424242');
cy.getWithinIframe('[name="exp-date"]').type('1230');
cy.getWithinIframe('[name="cvc"]').type('100');
cy.getWithinIframe('[name="postal"]').type('12345');
cy.get('#card-button').click();
cy.get('#errors').should('be.empty');
cy.location('pathname').should('eq', '/client/payment_methods');
});
it('should be able to complete payment with added credit card', () => {
cy.visit('/client/invoices');
cy.get('#unpaid-checkbox').click();
cy.get('[data-cy=pay-now')
.first()
.click();
cy.location('pathname').should('eq', '/client/invoices/payment');
cy.get('[data-cy=payment-methods-dropdown').click();
cy.get('[data-cy=payment-method')
.first()
.click();
cy.get('#pay-now-with-token').click();
cy.url().should('contain', '/client/payments');
});
});

View File

@ -23,15 +23,17 @@
// //
// -- This will overwrite an existing command -- // -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
const axios = require('axios');
const fixture = require('../fixtures/example.json');
Cypress.Commands.add('clientLogin', () => { Cypress.Commands.add('clientLogin', () => {
cy.visit('/client/login'); cy.visit('/client/login');
cy.get('#test_email') cy.get('#test_email')
.invoke('val') .invoke('val')
.then(emailValue => { .then((emailValue) => {
cy.get('#test_password') cy.get('#test_password')
.invoke('val') .invoke('val')
.then(passwordValue => { .then((passwordValue) => {
cy.get('#email') cy.get('#email')
.type(emailValue) .type(emailValue)
.should('have.value', emailValue); .should('have.value', emailValue);
@ -45,32 +47,62 @@ Cypress.Commands.add('clientLogin', () => {
}); });
}); });
Cypress.Commands.add( Cypress.Commands.add('iframeLoaded', { prevSubject: 'element' }, ($iframe) => {
'iframeLoaded', const contentWindow = $iframe.prop('contentWindow');
{prevSubject: 'element'}, return new Promise((resolve) => {
($iframe) => { if (contentWindow) {
const contentWindow = $iframe.prop('contentWindow'); resolve(contentWindow);
return new Promise(resolve => { } else {
if ( $iframe.on('load', () => {
contentWindow resolve(contentWindow);
) { });
resolve(contentWindow) }
} else {
$iframe.on('load', () => {
resolve(contentWindow)
})
}
})
}); });
});
Cypress.Commands.add( Cypress.Commands.add(
'getInDocument', 'getInDocument',
{prevSubject: 'Permission denied to access property "document" on cross-origin object'}, {
prevSubject:
'Permission denied to access property "document" on cross-origin object',
},
(document, selector) => Cypress.$(selector, document) (document, selector) => Cypress.$(selector, document)
); );
Cypress.Commands.add( Cypress.Commands.add('getWithinIframe', (targetElement) =>
'getWithinIframe', cy
(targetElement) => cy.get('iframe').iframeLoaded().its('document').getInDocument(targetElement) .get('iframe')
.iframeLoaded()
.its('document')
.getInDocument(targetElement)
); );
Cypress.Commands.add('useGateway', (gateway) => {
let body = {
settings: {
entity: 'App\\Models\\Client',
industry_id: '',
size_id: '',
currency_id: '1',
company_gateway_ids: gateway,
},
};
let options = {
headers: {
'X-Api-Secret': 'superdoopersecrethere',
'X-Api-Token':
'S0x8behDk8HG8PI0i8RXdpf2AVud5b993pE8vata7xmm4RgW6u3NeGC8ibWIUjZv',
'X-Requested-With': 'XMLHttpRequest',
},
};
axios
.put(
`http://localhost:8000/api/v1/clients/${fixture.first}`,
body,
options
)
.then((response) => console.log(response))
.catch((error) => console.log(error.message));
});

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -3,36 +3,36 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache'; const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache'; const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = { const RESOURCES = {
"version.json": "da8e821a4d19db954d411e3234e781d6", "assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
"main.dart.js": "9daccb2bcb9c8f21aa76580004feddd0",
"assets/NOTICES": "23c057eefb1ee72da3e32d88380ea642",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "51c686b86bc12382579d8283b7e76b6b",
"assets/fonts/MaterialIcons-Regular.otf": "132a5e63b5e510933ab4845577716106",
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2", "assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08", "assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024", "assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541", "assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
"assets/AssetManifest.json": "ea09ed4b9b8b6c83d6896248aac7c527", "assets/AssetManifest.json": "ea09ed4b9b8b6c83d6896248aac7c527",
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "51c686b86bc12382579d8283b7e76b6b",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/NOTICES": "23c057eefb1ee72da3e32d88380ea642",
"favicon.png": "dca91c54388f52eded692718d5a98b8b", "favicon.png": "dca91c54388f52eded692718d5a98b8b",
"/": "23224b5e03519aaa87594403d54412cf", "version.json": "145e9ca4d5ac36ac8b2776b39b3856cc",
"favicon.ico": "51636d3a390451561744c42188ccd628", "main.dart.js": "019d5fb0183e30407b4467703b079e49",
"manifest.json": "77215c1737c7639764e64a192be2f7b8", "manifest.json": "77215c1737c7639764e64a192be2f7b8",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35", "favicon.ico": "51636d3a390451561744c42188ccd628",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed" "/": "23224b5e03519aaa87594403d54412cf",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35"
}; };
// The application shell files that are downloaded before a service worker can // The application shell files that are downloaded before a service worker can

246701
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"/js/app.js": "/js/app.js?id=a33a5a58bfc6e2174841", "/js/app.js": "/js/app.js?id=a33a5a58bfc6e2174841",
"/css/app.css": "/css/app.css?id=b2e7d49a848e3cfb6370", "/css/app.css": "/css/app.css?id=fc37092c9a39881e5e2e",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=b0f29d5fdfa492962c22", "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=b0f29d5fdfa492962c22",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=d7e708d66a9c769b4c6e", "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=d7e708d66a9c769b4c6e",
"/js/clients/payment_methods/authorize-ach.js": "/js/clients/payment_methods/authorize-ach.js?id=c73d32c192c36fe44123", "/js/clients/payment_methods/authorize-ach.js": "/js/clients/payment_methods/authorize-ach.js?id=c73d32c192c36fe44123",

View File

@ -1 +1 @@
{"app_name":"invoiceninja_flutter","version":"5.0.17","build_number":"17"} {"app_name":"invoiceninja_flutter","version":"5.0.18","build_number":"18"}

View File

@ -3283,5 +3283,7 @@ return [
'saved_at' => 'Saved at :time', 'saved_at' => 'Saved at :time',
'credit_payment' => 'Credit applied to Invoice :invoice_number', 'credit_payment' => 'Credit applied to Invoice :invoice_number',
'credit_subject' => 'New credit :number from :account',
'credit_message' => 'To view your credit for :amount, click the link below.',
]; ];

View File

@ -99,7 +99,7 @@
@csrf @csrf
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}"> <input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="action" value="payment"> <input type="hidden" name="action" value="payment">
<button class="px-2 py-1 mr-3 text-xs uppercase button button-primary bg-primary"> <button class="px-2 py-1 mr-3 text-xs uppercase button button-primary bg-primary" data-cy="pay-now">
@lang('texts.pay_now') @lang('texts.pay_now')
</button> </button>
</form> </form>

View File

@ -12,16 +12,16 @@
<div class="relative" x-data="{ open: false }" x-on:click.away="open = false"> <div class="relative" x-data="{ open: false }" x-on:click.away="open = false">
<!-- Add payment method button --> <!-- Add payment method button -->
@if($client->getCreditCardGateway() || $client->getBankTransferGateway()) @if($client->getCreditCardGateway() || $client->getBankTransferGateway())
<button x-on:click="open = !open" class="button button-primary bg-primary">{{ ctrans('texts.add_payment_method') }}</button> <button x-on:click="open = !open" class="button button-primary bg-primary" data-cy="add-payment-method">{{ ctrans('texts.add_payment_method') }}</button>
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg"> <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg">
<div class="py-1 rounded-md bg-white shadow-xs"> <div class="py-1 rounded-md bg-white shadow-xs">
@if($client->getCreditCardGateway()) @if($client->getCreditCardGateway())
<a href="{{ route('client.payment_methods.create', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150"> <a data-cy="add-credit-card-link" href="{{ route('client.payment_methods.create', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.credit_card') }} {{ ctrans('texts.credit_card') }}
</a> </a>
@endif @endif
@if($client->getBankTransferGateway()) @if($client->getBankTransferGateway())
<a href="{{ route('client.payment_methods.create', ['method' => $client->getBankTransferMethodType()]) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150"> <a data-cy="add-bank-account-link" href="{{ route('client.payment_methods.create', ['method' => $client->getBankTransferMethodType()]) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.bank_account') }} {{ ctrans('texts.bank_account') }}
</a> </a>
@endif @endif
@ -91,7 +91,7 @@
</svg> </svg>
@endif @endif
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap flex items-center justify-end text-sm leading-5 font-medium"> <td class="px-6 py-4 whitespace-no-wrap flex items-center justify-end text-sm leading-5 font-medium" data-cy="view-payment-method">
<a href="{{ route('client.payment_methods.show', $payment_method->hashed_id) }}" <a href="{{ route('client.payment_methods.show', $payment_method->hashed_id) }}"
class="text-blue-600 hover:text-indigo-900 focus:outline-none focus:underline"> class="text-blue-600 hover:text-indigo-900 focus:outline-none focus:underline">
@lang('texts.view') @lang('texts.view')

View File

@ -0,0 +1,37 @@
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.subtotal') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['invoice_totals'], $client) }}
</dd>
@if($total['fee_total'] > 0)
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.gateway_fees') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['fee_total'], $client) }}
</dd>
@endif
@if($total['credit_totals'] > 0)
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.credit_amount') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['credit_totals'], $client) }}
</dd>
@endif
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount_due') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['amount_with_fee'], $client) }}
</dd>
</div>

View File

@ -2,96 +2,51 @@
@section('meta_title', ctrans('texts.add_credit_card')) @section('meta_title', ctrans('texts.add_credit_card'))
@push('head') @push('head')
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}"> <meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
@endpush @endpush
@section('body') @section('body')
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" method="post" id="server_response"> <form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" method="post" id="server_response">
@csrf @csrf
<input type="hidden" name="company_gateway_id" value="{{ $gateway->gateway_id }}"> <input type="hidden" name="company_gateway_id" value="{{ $gateway->gateway_id }}">
<input type="hidden" name="payment_method_id" value="1"> <input type="hidden" name="payment_method_id" value="1">
<input type="hidden" name="gateway_response" id="gateway_response"> <input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="is_default" id="is_default"> <input type="hidden" name="is_default" id="is_default">
</form> </form>
<div class="container mx-auto">
<div class="grid grid-cols-6 gap-4"> <div class="container mx-auto">
<div class="col-span-6 md:col-start-2 md:col-span-4"> <div class="grid grid-cols-6 gap-4">
<div class="alert alert-failure mb-4" hidden id="errors"></div> <div class="col-span-6 md:col-start-2 md:col-span-4">
<div class="bg-white shadow overflow-hidden sm:rounded-lg"> <div class="alert alert-failure mb-4" hidden id="errors"></div>
<div class="px-4 py-5 border-b border-gray-200 sm:px-6"> <div class="bg-white shadow overflow-hidden sm:rounded-lg">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <div class="px-4 py-5 border-b border-gray-200 sm:px-6">
{{ ctrans('texts.add_credit_card') }} <h3 class="text-lg leading-6 font-medium text-gray-900">
</h3> {{ ctrans('texts.add_credit_card') }}
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500"> </h3>
{{ ctrans('texts.authorize_for_future_use') }} <p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
</p> {{ ctrans('texts.authorize_for_future_use') }}
</div> </p>
<div> </div>
<dl> <div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center"> @include('portal.ninja2020.gateways.stripe.includes.card_widget')
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ ctrans('texts.name') }} <div class="bg-white px-4 py-5 flex justify-end">
</dt> <button type="button" id="card-button" data-secret="{{ $intent->client_secret }}" class="button button-primary bg-primary">
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> <svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<input class="input w-full" id="cardholder-name" type="text" placeholder="{{ ctrans('texts.name') }}"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
</dd> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</div><div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> </svg>
<dt class="text-sm leading-5 font-medium text-gray-500"> <span>{{ __('texts.save') }}</span>
{{ ctrans('texts.credit_card') }} </button>
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<div id="card-element"></div>
</dd>
</div>
<div class="{{ ($gateway->token_billing == 'optin' || $gateway->token_billing == 'optout') ? 'sm:grid' : 'hidden' }} bg-gray-50 px-4 py-5 sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.token_billing_checkbox') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<label class="mr-4">
<input
type="radio"
class="form-radio cursor-pointer"
name="token-billing-checkbox"
id="proxy_is_default"
value="true"
{{ ($gateway->token_billing == 'always' || $gateway->token_billing == 'optout') ? 'checked' : '' }} />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.yes') }}</span>
</label>
<label>
<input
type="radio"
class="form-radio cursor-pointer"
name="token-billing-checkbox"
id="proxy_is_default"
value="false"
{{ ($gateway->token_billing == 'off' || $gateway->token_billing == 'optin') ? 'checked' : '' }} />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.no') }}</span>
</label>
</dd>
</div>
<div class="bg-white px-4 py-5 flex justify-end">
<button
type="button"
id="card-button"
data-secret="{{ $intent->client_secret }}"
class="button button-primary bg-primary">
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ __('texts.save') }}</span>
</button>
</div>
</dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
@endsection @endsection
@push('footer') @push('footer')
<script src="https://js.stripe.com/v3/"></script> <script src="https://js.stripe.com/v3/"></script>
<script src="{{ asset('js/clients/payment_methods/authorize-stripe-card.js') }}"></script> <script src="{{ asset('js/clients/payment_methods/authorize-stripe-card.js') }}"></script>
@endpush @endpush

View File

@ -16,13 +16,10 @@
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}"> <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="payment_method_id" value="{{ $payment_method_id }}">
</form> </form>
<form action="{{route('client.payments.credit_response')}}" method="post" id="credit-payment">
@csrf
<input type="hidden" name="payment_hash" value="{{$payment_hash}}">
</form>
<div class="container mx-auto"> <div class="container mx-auto">
<div class="grid grid-cols-6 gap-4"> <div class="grid grid-cols-6 gap-4">
<div class="col-span-6 md:col-start-2 md:col-span-4"> <div class="col-span-6 md:col-start-2 md:col-span-4">
<div class="alert alert-failure mb-4" hidden id="errors"></div> <div class="alert alert-failure mb-4" hidden id="errors"></div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg"> <div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6"> <div class="px-4 py-5 border-b border-gray-200 sm:px-6">
@ -35,125 +32,22 @@
</div> </div>
<div> <div>
<dl> <dl>
@include('portal.ninja2020.gateways.includes.payment_details')
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.subtotal') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['invoice_totals'], $client) }}
</dd>
@if($total['fee_total'] > 0)
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.gateway_fees') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['fee_total'], $client) }}
</dd>
@endif
@if($total['credit_totals'] > 0)
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.credit_amount') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['credit_totals'], $client) }}
</dd>
@endif
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount_due') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['amount_with_fee'], $client) }}
</dd>
</div>
@if((int)$total['amount_with_fee'] == 0) @if((int)$total['amount_with_fee'] == 0)
<!-- finalize with credits only --> @include('portal.ninja2020.gateways.stripe.includes.pay_with_credit')
<div class="bg-white px-4 py-5 flex justify-end">
<button form="credit-payment" class="button button-primary bg-primary inline-flex items-center">Pay with credit</button>
</div>
@elseif($token) @elseif($token)
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center"> @include('portal.ninja2020.gateways.stripe.includes.pay_with_token')
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ ctrans('texts.credit_card') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ strtoupper($token->meta->brand) }} - **** {{ $token->meta->last4 }}
</dd>
</div>
<div class="bg-white px-4 py-5 flex justify-end">
<button
type="button"
data-secret="{{ $intent->client_secret }}"
data-token="{{ $token->token }}"
id="pay-now-with-token"
class="button button-primary bg-primary inline-flex items-center">
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ __('texts.save') }}</span>
</button>
</div>
@else @else
<div @include('portal.ninja2020.gateways.stripe.includes.card_widget')
class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ ctrans('texts.name') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="cardholder-name" type="text"
placeholder="{{ ctrans('texts.name') }}">
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.credit_card') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<div id="card-element"></div>
</dd>
</div>
<div class="{{ ($gateway->company_gateway->token_billing == 'optin' || $gateway->company_gateway->token_billing == 'optout') ? 'sm:grid' : 'hidden' }} bg-gray-50 px-4 py-5 sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.token_billing_checkbox') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<label class="mr-4">
<input
type="radio"
class="form-radio cursor-pointer"
name="token-billing-checkbox"
id="proxy_is_default"
value="true"
{{ ($gateway->company_gateway->token_billing == 'always' || $gateway->company_gateway->token_billing == 'optout') ? 'checked' : '' }} />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.yes') }}</span>
</label>
<label>
<input
type="radio"
class="form-radio cursor-pointer"
name="token-billing-checkbox"
id="proxy_is_default"
value="false"
{{ ($gateway->company_gateway->token_billing == 'off' || $gateway->company_gateway->token_billing == 'optin') ? 'checked' : '' }} />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.no') }}</span>
</label>
</dd>
</div>
<div class="bg-white px-4 py-5 flex justify-end"> <div class="bg-white px-4 py-5 flex justify-end">
<button <button type="button" id="pay-now" data-secret="{{ $intent->client_secret }}" class="button button-primary bg-primary">
type="button"
id="pay-now"
data-secret="{{ $intent->client_secret }}"
class="button button-primary bg-primary">
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
<span>{{ __('texts.save') }}</span> <span>{{ __('texts.pay_now') }}</span>
</button> </button>
</div> </div>
@endif @endif

View File

@ -0,0 +1,39 @@
@unless(isset($show_name) && $show_name == false)
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ ctrans('texts.name') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="cardholder-name" type="text" placeholder="{{ ctrans('texts.name') }}">
</dd>
</div>
@endunless
@unless(isset($show_card_element) && $show_card_element == false)
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.credit_card') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<div id="card-element"></div>
</dd>
</div>
@endunless
@unless(isset($show_save) && $show_save == false)
<div class="{{ ($gateway->token_billing == 'optin' || $gateway->token_billing == 'optout') ? 'sm:grid' : 'hidden' }} bg-gray-50 px-4 py-5 sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.token_billing_checkbox') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<label class="mr-4">
<input type="radio" class="form-radio cursor-pointer" name="token-billing-checkbox" id="proxy_is_default" value="true" {{ ($gateway->token_billing == 'always' || $gateway->token_billing == 'optout') ? 'checked' : '' }} />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.yes') }}</span>
</label>
<label>
<input type="radio" class="form-radio cursor-pointer" name="token-billing-checkbox" id="proxy_is_default" value="false" {{ ($gateway->token_billing == 'off' || $gateway->token_billing == 'optin') ? 'checked' : '' }} />
<span class="ml-1 cursor-pointer">{{ ctrans('texts.no') }}</span>
</label>
</dd>
</div>
@endunless

View File

@ -0,0 +1,8 @@
<form action="{{ route('client.payments.credit_response') }}" method="post" id="credit-payment">
@csrf
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
</form>
<div class="bg-white px-4 py-5 flex justify-end">
<button form="credit-payment" class="button button-primary bg-primary inline-flex items-center">Pay with credit</button>
</div>

View File

@ -0,0 +1,17 @@
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ ctrans('texts.credit_card') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ strtoupper($token->meta->brand) }} - **** {{ $token->meta->last4 }}
</dd>
</div>
<div class="bg-white px-4 py-5 flex justify-end">
<button type="button" data-secret="{{ $intent->client_secret }}" data-token="{{ $token->token }}" id="pay-now-with-token" class="button button-primary bg-primary inline-flex items-center">
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ __('texts.pay_now') }}</span>
</button>
</div>

View File

@ -21,7 +21,7 @@
<div class="flex justify-end mb-2"> <div class="flex justify-end mb-2">
<!-- Pay now button --> <!-- Pay now button -->
@if(count($payment_methods) > 0) @if(count($payment_methods) > 0)
<div x-data="{ open: false }" @keydown.window.escape="open = false" @click.away="open = false" class="relative inline-block text-left"> <div x-data="{ open: false }" @keydown.window.escape="open = false" @click.away="open = false" class="relative inline-block text-left" data-cy="payment-methods-dropdown">
<div> <div>
<div class="rounded-md shadow-sm"> <div class="rounded-md shadow-sm">
<button @click="open = !open" type="button" class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800"> <button @click="open = !open" type="button" class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800">
@ -36,7 +36,7 @@
<div class="bg-white rounded-md shadow-xs"> <div class="bg-white rounded-md shadow-xs">
<div class="py-1"> <div class="py-1">
@foreach($payment_methods as $payment_method) @foreach($payment_methods as $payment_method)
<a href="#" @click="{ open = false }" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"> <a href="#" @click="{ open = false }" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
{{ $payment_method['label'] }} {{ $payment_method['label'] }}
</a> </a>
@endforeach @endforeach

View File

@ -36,7 +36,7 @@
<form action="{{ route('client.payment_methods.destroy', [$payment_method->hashed_id, 'method' => $payment_method->gateway_type->id]) }}" method="post"> <form action="{{ route('client.payment_methods.destroy', [$payment_method->hashed_id, 'method' => $payment_method->gateway_type->id]) }}" method="post">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="button button-danger button-block"> <button type="submit" class="button button-danger button-block" data-cy="confirm-payment-removal">
{{ ctrans('texts.remove') }} {{ ctrans('texts.remove') }}
</button> </button>
</form> </form>

View File

@ -98,7 +98,7 @@
</div> </div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center"> <div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }"> <div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }">
<button class="button button-danger" translate @click="open = true"> <button class="button button-danger" @click="open = true" id="open-delete-popup">
{{ ctrans('texts.remove_payment_method') }} {{ ctrans('texts.remove_payment_method') }}
</button> </button>
@include('portal.ninja2020.payment_methods.includes.modals.removal') @include('portal.ninja2020.payment_methods.includes.modals.removal')

View File

@ -31,7 +31,7 @@ Route::group(['middleware' => ['auth:contact', 'locale'], 'prefix' => 'client',
Route::get('invoices/{invoice_invitation}', 'ClientPortal\InvoiceController@show')->name('invoice.show_invitation'); Route::get('invoices/{invoice_invitation}', 'ClientPortal\InvoiceController@show')->name('invoice.show_invitation');
Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index')->middleware('portal_enabled'); Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index')->middleware('portal_enabled');
Route::get('recurring_invoices/{recurring_invoice}', 'ClientPortal\RecurringInvoiceController@show')->name('recurring_invoices.show'); Route::get('recurring_invoices/{recurring_invoice}', 'ClientPortal\RecurringInvoiceController@show')->name('recurring_invoice.show');
Route::get('recurring_invoices/{recurring_invoice}/request_cancellation', 'ClientPortal\RecurringInvoiceController@requestCancellation')->name('recurring_invoices.request_cancellation'); Route::get('recurring_invoices/{recurring_invoice}/request_cancellation', 'ClientPortal\RecurringInvoiceController@requestCancellation')->name('recurring_invoices.request_cancellation');
Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process'); Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process');

View File

@ -99,6 +99,8 @@ class ShopInvoiceTest extends TestCase
$this->company->enable_shop_api = true; $this->company->enable_shop_api = true;
$this->company->save(); $this->company->save();
Product::truncate();
$product = Product::factory()->create([ $product = Product::factory()->create([
'user_id' => $this->user->id, 'user_id' => $this->user->id,
'company_id' => $this->company->id, 'company_id' => $this->company->id,

View File

@ -333,6 +333,14 @@ trait MockAccountData
$this->credit->amount = 10; $this->credit->amount = 10;
$this->credit->balance = 10; $this->credit->balance = 10;
$this->credit->tax_name1 = '';
$this->credit->tax_name2 = '';
$this->credit->tax_name3 = '';
$this->credit->tax_rate1 = 0;
$this->credit->tax_rate2 = 0;
$this->credit->tax_rate3 = 0;
$this->credit->uses_inclusive_taxes = false; $this->credit->uses_inclusive_taxes = false;
$this->credit->save(); $this->credit->save();