1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-27 19:57:10 +02:00

Merge pull request #6290 from turbo124/v5-stable

v5.2.13
This commit is contained in:
David Bomba 2021-07-18 14:19:40 +10:00 committed by GitHub
commit beeb19d6a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
206 changed files with 206324 additions and 206349 deletions

View File

@ -5,7 +5,7 @@ APP_DEBUG=true
APP_URL=http://ninja.test APP_URL=http://ninja.test
MULTI_DB_ENABLED=false MULTI_DB_ENABLED=false
# database # database
DB_CONNECTION=db-ninja-01 DB_CONNECTION=mysql
DB_DATABASE1=ninja DB_DATABASE1=ninja
DB_USERNAME1=root DB_USERNAME1=root
DB_PASSWORD1=ninja DB_PASSWORD1=ninja

View File

@ -1,6 +1,16 @@
# Release notes # Release notes
## [Unreleased (daily channel)](https://github.com/invoiceninja/invoiceninja/tree/v5-develop) ## [Unreleased (daily channel)](https://github.com/invoiceninja/invoiceninja/tree/v5-develop)
## Added:
- Client portal: Show message when trying to approve non-approvable quotes
- Client portal: Remove "Approve" button from single quote page if quote is non-approvable
- Client portal: Hide "Pay now" buttons if no gateways are configured
- Client portal: "Download" and "Open in new tab" buttons on documents show page
- Client portal: Make "Invoice Ninja" link clickable in footer
## Fixed:
- Client portal: Showing message instead of blank page when trying to download zero quotes
- Client portal: Fixed bug with payment gateways after checking for required fields
## [v5.2.0-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.2.0-release) ## [v5.2.0-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.2.0-release)
## Added: ## Added:

View File

@ -6,15 +6,30 @@
![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable) ![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/d16c78aad8574466bf83232b513ef4fb)](https://www.codacy.com/gh/turbo124/invoiceninja/dashboard?utm_source=github.com&utm_medium=referral&utm_content=turbo124/invoiceninja&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d16c78aad8574466bf83232b513ef4fb)](https://www.codacy.com/gh/turbo124/invoiceninja/dashboard?utm_source=github.com&utm_medium=referral&utm_content=turbo124/invoiceninja&utm_campaign=Badge_Grade)
# Invoice Ninja version 5! # Invoice Ninja 5
## Preamble ## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org)
### We're on Slack, join us at [slack.invoiceninja.com](http://slack.invoiceninja.com), [forum.invoiceninja.com](https://forum.invoiceninja.com) or if you like [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/)
Just make sure to add the `invoice-ninja` tag to your question.
## Introduction
Version 5 of Invoice Ninja is here! We've taken the best parts of version 4 and bolted on all of the most requested features to produce a invoicing application like no other. Version 5 of Invoice Ninja is here! We've taken the best parts of version 4 and bolted on all of the most requested features to produce a invoicing application like no other.
The new interface has a lot more functionality so it isn't a carbon copy of v4, but once you get used to the new layout and functionality we are sure you will love it! The new interface has a lot more functionality so it isn't a carbon copy of v4, but once you get used to the new layout and functionality we are sure you will love it!
If you have any questions, please join us on our [forum](https://forum.invoiceninja.com) or on [slack](https://invoiceninja.slack.com) ## Referral Program
* Earn 50% of Pro & Enterprise Plans up to 4 years - [Learn more](https://www.invoiceninja.com/referral-program/)
## Recommended Providers
* [Stripe](https://stripe.com/)
* [Postmark](https://postmarkapp.com/)
## Development
* [API Documentation](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
* [APP Documentation](https://invoiceninja.github.io/)
## Quick Start ## Quick Start
@ -90,16 +105,6 @@ Installation, Configuration and Troubleshooting documentation can be found [HERE
* [Clemens Mol](https://github.com/clemensmol) * [Clemens Mol](https://github.com/clemensmol)
* [Benjamin Beganović](https://github.com/beganovich) * [Benjamin Beganović](https://github.com/beganovich)
## Current work in progress
Invoice Ninja is written in a combination of technologies:
API - Laravel
Client Portal - Laravel + Tailwind
Admin Portal - Flutter
This allows an immersive and consistent experience across any device: mobile, tablet or desktop.
## Security ## Security
If you find a security issue with this application please send an email to contact@invoiceninja.com Please follow responsible disclosure procedures if you detect an issue. For further information on responsible disclosure please read [here](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html) If you find a security issue with this application please send an email to contact@invoiceninja.com Please follow responsible disclosure procedures if you detect an issue. For further information on responsible disclosure please read [here](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html)

View File

@ -1 +1 @@
5.2.11 5.2.13

View File

@ -69,7 +69,7 @@ class CheckData extends Command
/** /**
* @var string * @var string
*/ */
protected $signature = 'ninja:check-data {--database=} {--fix=} {--client_id=} {--paid_to_date=}'; protected $signature = 'ninja:check-data {--database=} {--fix=} {--client_id=} {--paid_to_date=} {--client_balance=}';
/** /**
* @var string * @var string
@ -506,6 +506,7 @@ class CheckData extends Command
$this->logMessage($client->present()->name.' - '.$client->id." - calculated client balances do not match Invoice Balances = {$invoice_balance} - Client Balance = ".rtrim($client->balance, '0'). " Ledger balance = {$ledger->balance}"); $this->logMessage($client->present()->name.' - '.$client->id." - calculated client balances do not match Invoice Balances = {$invoice_balance} - Client Balance = ".rtrim($client->balance, '0'). " Ledger balance = {$ledger->balance}");
$this->isValid = false; $this->isValid = false;
} }
} }
@ -535,6 +536,20 @@ class CheckData extends Command
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}"); $this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}");
$this->isValid = false; $this->isValid = false;
if($this->option('client_balance')){
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}");
$client->balance = $invoice_balance;
$client->save();
$ledger->adjustment = $invoice_balance;
$ledger->balance = $invoice_balance;
$ledger->notes = 'Ledger Adjustment';
$ledger->save();
}
} }
} }

View File

@ -14,9 +14,11 @@ namespace App\Console\Commands;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits; use App\DataMapper\FeesAndLimits;
use App\Events\Invoice\InvoiceWasCreated; use App\Events\Invoice\InvoiceWasCreated;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
use App\Factory\GroupSettingFactory; use App\Factory\GroupSettingFactory;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory; use App\Factory\InvoiceItemFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Factory\SubscriptionFactory; use App\Factory\SubscriptionFactory;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyTaskStatuses;
@ -48,6 +50,7 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use stdClass;
class CreateSingleAccount extends Command class CreateSingleAccount extends Command
{ {
@ -117,7 +120,7 @@ class CreateSingleAccount extends Command
$company->settings = $settings; $company->settings = $settings;
$company->save(); $company->save();
$account->default_company_id = $company->id; $account->default_company_id = $company->id;
$account->save(); $account->save();
@ -165,7 +168,7 @@ class CreateSingleAccount extends Command
TaxRate::factory()->create([ TaxRate::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'name' => 'VAT', 'name' => 'VAT',
'rate' => 17.5 'rate' => 17.5
]); ]);
@ -176,7 +179,7 @@ class CreateSingleAccount extends Command
'name' => 'CA Sales Tax', 'name' => 'CA Sales Tax',
'rate' => 5 'rate' => 5
]); ]);
$this->info('Creating '.$this->count.' clients'); $this->info('Creating '.$this->count.' clients');
@ -225,16 +228,19 @@ class CreateSingleAccount extends Command
$client = $company->clients->random(); $client = $company->clients->random();
$this->info('creating task for client #'.$client->id); $this->info('creating task for client #' . $client->id);
$this->createTask($client); $this->createTask($client);
$client = $company->clients->random(); $client = $company->clients->random();
$this->info('creating project for client #'.$client->id); $this->info('creating project for client #' . $client->id);
$this->createProject($client); $this->createProject($client);
$this->info('creating credit for client #'.$client->id); $this->info('creating credit for client #' . $client->id);
$this->createCredit($client); $this->createCredit($client);
$this->info('creating recurring invoice for client # ' . $client->id);
$this->createRecurringInvoice($client);
} }
$this->createGateways($company, $user); $this->createGateways($company, $user);
@ -249,34 +255,34 @@ class CreateSingleAccount extends Command
$gs->save(); $gs->save();
$p1 = Product::factory()->create([ $p1 = Product::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'product_key' => 'pro_plan', 'product_key' => 'pro_plan',
'notes' => 'The Pro Plan', 'notes' => 'The Pro Plan',
'cost' => 10, 'cost' => 10,
'price' => 10, 'price' => 10,
'quantity' => 1, 'quantity' => 1,
]); ]);
$p2 = Product::factory()->create([ $p2 = Product::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'product_key' => 'enterprise_plan', 'product_key' => 'enterprise_plan',
'notes' => 'The Enterprise Plan', 'notes' => 'The Enterprise Plan',
'cost' => 14, 'cost' => 14,
'price' => 14, 'price' => 14,
'quantity' => 1, 'quantity' => 1,
]); ]);
$p3 = Product::factory()->create([ $p3 = Product::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'product_key' => 'free_plan', 'product_key' => 'free_plan',
'notes' => 'The Free Plan', 'notes' => 'The Free Plan',
'cost' => 0, 'cost' => 0,
'price' => 0, 'price' => 0,
'quantity' => 1, 'quantity' => 1,
]); ]);
$webhook_config = [ $webhook_config = [
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan', 'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
@ -435,6 +441,10 @@ class CreateSingleAccount extends Command
$invoice = $invoice_calc->getInvoice(); $invoice = $invoice_calc->getInvoice();
if ($this->gateway === 'braintree') {
$invoice->amount = 100; // Braintree sandbox only allows payments under 2,000 to complete successfully.
}
$invoice->save(); $invoice->save();
$invoice->service()->createInvitations()->markSent(); $invoice->service()->createInvitations()->markSent();
@ -619,7 +629,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -642,7 +652,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -663,7 +673,7 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
@ -684,11 +694,96 @@ class CreateSingleAccount extends Command
$gateway_types = $cg->driver(new Client)->gatewayTypes(); $gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new \stdClass; $fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.wepay') && ($this->gateway == 'all' || $this->gateway == 'wepay')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '8fdeed552015b3c7b44ed6c8ebd9e992';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.wepay'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.braintree') && ($this->gateway == 'all' || $this->gateway == 'braintree')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = 'f7ec488676d310683fb51802d076d713';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.braintree'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits; $cg->fees_and_limits = $fees_and_limits;
$cg->save(); $cg->save();
} }
} }
private function createRecurringInvoice($client)
{
$faker = Factory::create();
$invoice = RecurringInvoiceFactory::create($client->company->id, $client->user->id); //stub the company and user_id
$invoice->client_id = $client->id;
$dateable = Carbon::now()->subDays(rand(0, 90));
$invoice->date = $dateable;
$invoice->line_items = $this->buildLineItems(rand(1, 10));
$invoice->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$invoice->tax_name1 = 'GST';
$invoice->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$invoice->tax_name2 = 'VAT';
$invoice->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$invoice->tax_name3 = 'CA Sales Tax';
$invoice->tax_rate3 = 5;
}
$invoice->custom_value1 = $faker->date;
$invoice->custom_value2 = rand(0, 1) ? 'yes' : 'no';
$invoice->status_id = RecurringInvoice::STATUS_ACTIVE;
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
$invoice->save();
event(new RecurringInvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}
} }

View File

@ -22,7 +22,7 @@ class ExpenseCategoryFactory
$expense->company_id = $company_id; $expense->company_id = $company_id;
$expense->name = ''; $expense->name = '';
$expense->is_deleted = false; $expense->is_deleted = false;
$expense->color = '#fff'; $expense->color = '';
return $expense; return $expense;
} }

View File

@ -38,6 +38,8 @@ class RecurringInvoiceToInvoiceFactory
$invoice->tax_rate2 = $recurring_invoice->tax_rate2; $invoice->tax_rate2 = $recurring_invoice->tax_rate2;
$invoice->tax_name3 = $recurring_invoice->tax_name3; $invoice->tax_name3 = $recurring_invoice->tax_name3;
$invoice->tax_rate3 = $recurring_invoice->tax_rate3; $invoice->tax_rate3 = $recurring_invoice->tax_rate3;
$invoice->total_taxes = $recurring_invoice->total_taxes;
$invoice->subscription_id = $recurring_invoice->subscription_id;
$invoice->custom_value1 = $recurring_invoice->custom_value1; $invoice->custom_value1 = $recurring_invoice->custom_value1;
$invoice->custom_value2 = $recurring_invoice->custom_value2; $invoice->custom_value2 = $recurring_invoice->custom_value2;
$invoice->custom_value3 = $recurring_invoice->custom_value3; $invoice->custom_value3 = $recurring_invoice->custom_value3;

View File

@ -21,7 +21,7 @@ class TaskStatusFactory
$task_status->user_id = $user_id; $task_status->user_id = $user_id;
$task_status->company_id = $company_id; $task_status->company_id = $company_id;
$task_status->name = ''; $task_status->name = '';
$task_status->color = '#fff'; $task_status->color = '';
$task_status->status_order = 9999; $task_status->status_order = 9999;
return $task_status; return $task_status;

View File

@ -510,7 +510,7 @@ class ClientController extends BaseController
$action = request()->input('action'); $action = request()->input('action');
$ids = request()->input('ids'); $ids = request()->input('ids');
$clients = Client::withTrashed()->find($this->transformKeys($ids)); $clients = Client::withTrashed()->whereIn('id', $this->transformKeys($ids))->cursor();
$clients->each(function ($client, $key) use ($action) { $clients->each(function ($client, $key) use ($action) {
if (auth()->user()->can('edit', $client)) { if (auth()->user()->can('edit', $client)) {

View File

@ -243,6 +243,10 @@ class PaymentController extends Controller
->get(); ->get();
} }
if(!$is_credit_payment){
$credit_totals = 0;
}
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))]; $hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))];
if ($request->query('hash')) { if ($request->query('hash')) {
@ -257,11 +261,19 @@ class PaymentController extends Controller
$payment_hash->save(); $payment_hash->save();
if($is_credit_payment){
$amount_with_fee = max(0, (($invoice_totals + $fee_totals) - $credit_totals));
}
else{
$credit_totals = 0;
$amount_with_fee = max(0, $invoice_totals + $fee_totals);
}
$totals = [ $totals = [
'credit_totals' => $credit_totals, 'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals, 'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals, 'fee_total' => $fee_totals,
'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals)), 'amount_with_fee' => $amount_with_fee,
]; ];
$data = [ $data = [
@ -273,7 +285,7 @@ class PaymentController extends Controller
'amount_with_fee' => $invoice_totals + $fee_totals, 'amount_with_fee' => $invoice_totals + $fee_totals,
]; ];
if ($is_credit_payment) { if ($is_credit_payment || $totals <= 0) {
return $this->processCreditPayment($request, $data); return $this->processCreditPayment($request, $data);
} }

View File

@ -85,7 +85,9 @@ class QuoteController extends Controller
->get(); ->get();
if (! $quotes || $quotes->count() == 0) { if (! $quotes || $quotes->count() == 0) {
return; return redirect()
->route('client.quotes.index')
->with('message', ctrans('texts.no_quotes_available_for_download'));
} }
if ($quotes->count() == 1) { if ($quotes->count() == 1) {
@ -121,7 +123,9 @@ class QuoteController extends Controller
->get(); ->get();
if (!$quotes || $quotes->count() == 0) { if (!$quotes || $quotes->count() == 0) {
return redirect()->route('client.quotes.index'); return redirect()
->route('client.quotes.index')
->with('message', ctrans('texts.quotes_with_status_sent_can_be_approved'));
} }
if ($process) { if ($process) {

View File

@ -43,8 +43,7 @@ class SubscriptionPlanSwitchController extends Controller
*/ */
if(is_null($amount)) if(is_null($amount))
render('subscriptions.denied'); render('subscriptions.denied');
return render('subscriptions.switch', [ return render('subscriptions.switch', [
'subscription' => $recurring_invoice->subscription, 'subscription' => $recurring_invoice->subscription,
'recurring_invoice' => $recurring_invoice, 'recurring_invoice' => $recurring_invoice,

View File

@ -474,6 +474,10 @@ class CompanyController extends BaseController
*/ */
public function destroy(DestroyCompanyRequest $request, Company $company) public function destroy(DestroyCompanyRequest $request, Company $company)
{ {
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$company_count = $company->account->companies->count(); $company_count = $company->account->companies->count();
$account = $company->account; $account = $company->account;
$account_key = $account->key; $account_key = $account->key;
@ -577,4 +581,9 @@ class CompanyController extends BaseController
return $this->itemResponse($company->fresh()); return $this->itemResponse($company->fresh());
} }
// public function default(DefaultCompanyRequest $request, Company $company)
// {
// }
} }

View File

@ -0,0 +1,52 @@
<?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\Controllers;
use App\Jobs\Account\CreateAccount;
use App\Libraries\MultiDB;
use App\Models\CompanyToken;
use Illuminate\Http\Request;
class HostedMigrationController extends Controller
{
public function getAccount(Request $request)
{
if($request->header('X-API-HOSTED-SECRET') != config('ninja.ninja_hosted_secret'))
return;
if($user = MultiDB::hasUser(['email' => $request->input('email')]))
{
if($user->account->owner() && $user->account->companies()->count() >= 1)
{
return response()->json(['token' => $user->account->companies->first()->tokens->first()->token] ,200);
}
return response()->json(['error' => 'This user is not able to perform a migration. Please contact us at contact@invoiceninja.com to discuss.'], 401);
}
$account = CreateAccount::dispatchNow($request->all(), $request->getClientIp());
$company = $account->companies->first();
$company_token = CompanyToken::where('user_id', auth()->user()->id)
->where('company_id', $company->id)
->first();
return response()->json(['token' => $company_token->token], 200);
}
}

View File

@ -16,6 +16,7 @@ use App\Utils\CurlUtils;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use stdClass; use stdClass;
use Carbon\Carbon;
class LicenseController extends BaseController class LicenseController extends BaseController
{ {
@ -152,7 +153,7 @@ class LicenseController extends BaseController
{ {
$account = auth()->user()->company()->account; $account = auth()->user()->company()->account;
if($account->plan == 'white_label' && $account->plan_expires->lt(now())){ if($account->plan == 'white_label' && Carbon::parse($account->plan_expires)->lt(now())){
$account->plan = null; $account->plan = null;
$account->plan_expires = null; $account->plan_expires = null;
$account->save(); $account->save();

View File

@ -82,6 +82,9 @@ class MigrationController extends BaseController
*/ */
public function purgeCompany(Company $company) public function purgeCompany(Company $company)
{ {
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$account = $company->account; $account = $company->account;
$company_id = $company->id; $company_id = $company->id;
@ -102,6 +105,9 @@ class MigrationController extends BaseController
private function purgeCompanyWithForceFlag(Company $company) private function purgeCompanyWithForceFlag(Company $company)
{ {
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$account = $company->account; $account = $company->account;
$company_id = $company->id; $company_id = $company->id;

View File

@ -0,0 +1,36 @@
<?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\Controllers;
use App\Http\Requests\Payments\PaymentNotificationWebhookRequest;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Utils\Traits\MakesHash;
use Auth;
class PaymentNotificationWebhookController extends Controller
{
use MakesHash;
public function __invoke(PaymentNotificationWebhookRequest $request, string $company_key, string $company_gateway_id, string $client_hash)
{
$company_gateway = CompanyGateway::find($this->decodePrimaryKey($company_gateway_id));
$client = Client::find($this->decodePrimaryKey($client_hash));
return $company_gateway
->driver($client)
->processWebhookRequest($request);
}
}

View File

@ -470,7 +470,7 @@ class ProductController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$products = Product::withTrashed()->find($this->transformKeys($ids)); $products = Product::withTrashed()->whereIn('id', $this->transformKeys($ids))->cursor();
$products->each(function ($product, $key) use ($action) { $products->each(function ($product, $key) use ($action) {
if (auth()->user()->can('edit', $product)) { if (auth()->user()->can('edit', $product)) {

View File

@ -51,7 +51,7 @@ class StripeConnectController extends BaseController
$config = $company_gateway->getConfig(); $config = $company_gateway->getConfig();
if(property_exists($config, 'account_id')) if(property_exists($config, 'account_id') && strlen($config->account_id) > 1)
return view('auth.connect.existing'); return view('auth.connect.existing');
} }

View File

@ -32,6 +32,7 @@ class General extends Component
'first_name' => ['sometimes'], 'first_name' => ['sometimes'],
'last_name' => ['sometimes'], 'last_name' => ['sometimes'],
'email' => ['required', 'email'], 'email' => ['required', 'email'],
'phone' => ['sometimes'],
]; ];
public function mount() public function mount()

View File

@ -57,9 +57,6 @@ class PasswordProtection
$user = false; $user = false;
$google = new Google(); $google = new Google();
$user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD')); $user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD'));
nlog("user");
nlog($user);
if (is_array($user)) { if (is_array($user)) {
@ -68,8 +65,6 @@ class PasswordProtection
'oauth_provider_id'=> 'google' 'oauth_provider_id'=> 'google'
]; ];
nlog($query);
//If OAuth and user also has a password set - check both //If OAuth and user also has a password set - check both
if ($existing_user = MultiDB::hasUser($query) && auth()->user()->company()->oauth_password_required && auth()->user()->has_password && Hash::check(auth()->user()->password, $request->header('X-API-PASSWORD'))) { if ($existing_user = MultiDB::hasUser($query) && auth()->user()->company()->oauth_password_required && auth()->user()->has_password && Hash::check(auth()->user()->password, $request->header('X-API-PASSWORD'))) {

View File

@ -52,8 +52,8 @@ class QueryLogging
$timeEnd = microtime(true); $timeEnd = microtime(true);
$time = $timeEnd - $timeStart; $time = $timeEnd - $timeStart;
if($count > 150) // if($count > 150)
nlog($queries); // nlog($queries);
$ip = ''; $ip = '';

View File

@ -30,17 +30,22 @@ class UrlSetDb
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
if (config('ninja.db.multi_db_enabled')) { if (config('ninja.db.multi_db_enabled')) {
$hashids = new Hashids('', 10); //decoded output is _always_ an array. $hashids = new Hashids(config('ninja.hash_salt'), 10);
//parse URL hash and set DB //parse URL hash and set DB
$segments = explode('-', $request->route('confirmation_code')); $segments = explode('-', $request->route('confirmation_code'));
if(!is_array($segments))
return response()->json(['message' => 'Invalid confirmation code'], 403);
$hashed_db = $hashids->decode($segments[0]); $hashed_db = $hashids->decode($segments[0]);
MultiDB::setDB(MultiDB::DB_PREFIX.str_pad($hashed_db[0], 2, '0', STR_PAD_LEFT)); MultiDB::setDB(MultiDB::DB_PREFIX.str_pad($hashed_db[0], 2, '0', STR_PAD_LEFT));
} }
return $next($request); return $next($request);
} }
} }

View File

@ -46,7 +46,8 @@ class CreateAccountRequest extends Request
} }
protected function prepareForValidation() protected function prepareForValidation()
{nlog($this->all()); {
$input = $this->all(); $input = $this->all();
$input['user_agent'] = request()->server('HTTP_USER_AGENT'); $input['user_agent'] = request()->server('HTTP_USER_AGENT');

View File

@ -47,27 +47,32 @@ class StoreCompanyGatewayRequest extends Request
$gateway = Gateway::where('key', $input['gateway_key'])->first(); $gateway = Gateway::where('key', $input['gateway_key'])->first();
$default_gateway_fields = json_decode($gateway->fields); if($gateway);
{
/*Force gateway properties */ $default_gateway_fields = json_decode($gateway->fields);
if (isset($input['config']) && is_object(json_decode($input['config']))) {
foreach (json_decode($input['config']) as $key => $value) { /*Force gateway properties */
if (isset($input['config']) && is_object(json_decode($input['config']))) {
$default_gateway_fields->{$key} = $value; foreach (json_decode($input['config']) as $key => $value) {
$default_gateway_fields->{$key} = $value;
}
$input['config'] = json_encode($default_gateway_fields);
} }
$input['config'] = json_encode($default_gateway_fields); if (isset($input['config']))
$input['config'] = encrypt($input['config']);
if (isset($input['fees_and_limits']))
$input['fees_and_limits'] = $this->cleanFeesAndLimits($input['fees_and_limits']);
} }
if (isset($input['config']))
$input['config'] = encrypt($input['config']);
if (isset($input['fees_and_limits']))
$input['fees_and_limits'] = $this->cleanFeesAndLimits($input['fees_and_limits']);
$this->replace($input); $this->replace($input);
} }

View File

@ -59,7 +59,7 @@ class StoreExpenseRequest extends Request
} }
if(array_key_exists('color', $input) && is_null($input['color'])) if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff'; $input['color'] = '';
$this->replace($input); $this->replace($input);
} }

View File

@ -43,7 +43,7 @@ class StoreExpenseCategoryRequest extends Request
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
if(array_key_exists('color', $input) && is_null($input['color'])) if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff'; $input['color'] = '';
$this->replace($input); $this->replace($input);
} }

View File

@ -47,7 +47,7 @@ class UpdateExpenseCategoryRequest extends Request
$input = $this->all(); $input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color'])) if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff'; $input['color'] = '';
$this->replace($input); $this->replace($input);
} }

View File

@ -0,0 +1,42 @@
<?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\Payments;
use App\Http\Requests\Request;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Utils\Traits\MakesHash;
class PaymentNotificationWebhookRequest extends Request
{
use MakesHash;
public function authorize()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return true;
}
public function rules()
{
return [
//
];
}
}

View File

@ -51,7 +51,7 @@ class StoreProjectRequest extends Request
if(array_key_exists('color', $input) && is_null($input['color'])) if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff'; $input['color'] = '';
$this->replace($input); $this->replace($input);
} }

View File

@ -49,7 +49,7 @@ class UpdateProjectRequest extends Request
} }
if(array_key_exists('color', $input) && is_null($input['color'])) if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff'; $input['color'] = '';
$this->replace($input); $this->replace($input);
} }

View File

@ -51,7 +51,7 @@ class UpdateTaskRequest extends Request
} }
if(array_key_exists('color', $input) && is_null($input['color'])) if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff'; $input['color'] = '';
$this->replace($input); $this->replace($input);
} }

View File

@ -33,7 +33,7 @@ class StoreTaskStatusRequest extends Request
$input = $this->all(); $input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color'])) if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff'; $input['color'] = '';
$this->replace($input); $this->replace($input);
} }

View File

@ -46,7 +46,7 @@ class UpdateTaskStatusRequest extends Request
$input = $this->all(); $input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color'])) if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff'; $input['color'] = '';
$this->replace($input); $this->replace($input);
} }

View File

@ -45,14 +45,15 @@ class ClientMap
26 => 'client.vat_number', 26 => 'client.vat_number',
27 => 'client.id_number', 27 => 'client.id_number',
28 => 'client.public_notes', 28 => 'client.public_notes',
29 => 'contact.first_name', 29 => 'client.phone',
30 => 'contact.last_name', 30 => 'contact.first_name',
31 => 'contact.email', 31 => 'contact.last_name',
32 => 'contact.phone', 32 => 'contact.email',
33 => 'contact.custom_value1', 33 => 'contact.phone',
34 => 'contact.custom_value2', 34 => 'contact.custom_value1',
35 => 'contact.custom_value3', 35 => 'contact.custom_value2',
36 => 'contact.custom_value4', 36 => 'contact.custom_value3',
37 => 'contact.custom_value4',
]; ];
} }
@ -88,14 +89,15 @@ class ClientMap
26 => 'texts.vat_number', 26 => 'texts.vat_number',
27 => 'texts.id_number', 27 => 'texts.id_number',
28 => 'texts.public_notes', 28 => 'texts.public_notes',
29 => 'texts.first_name', 29 => 'texts.client_phone',
30 => 'texts.last_name', 30 => 'texts.first_name',
31 => 'texts.email', 31 => 'texts.last_name',
32 => 'texts.phone', 32 => 'texts.email',
33 => 'texts.custom_value', 33 => 'texts.phone',
34 => 'texts.custom_value', 34 => 'texts.custom_value',
35 => 'texts.custom_value', 35 => 'texts.custom_value',
36 => 'texts.custom_value', 36 => 'texts.custom_value',
37 => 'texts.custom_value',
]; ];
} }
} }

View File

@ -22,7 +22,7 @@ class ExpenseTransformer extends BaseTransformer {
'currency_id' => $this->getCurrencyByCode( $data, 'expense.currency_id' ), 'currency_id' => $this->getCurrencyByCode( $data, 'expense.currency_id' ),
'vendor_id' => isset( $data['expense.vendor'] ) ? $this->getVendorId( $data['expense.vendor'] ) : null, 'vendor_id' => isset( $data['expense.vendor'] ) ? $this->getVendorId( $data['expense.vendor'] ) : null,
'client_id' => isset( $data['expense.client'] ) ? $this->getClientId( $data['expense.client'] ) : null, 'client_id' => isset( $data['expense.client'] ) ? $this->getClientId( $data['expense.client'] ) : null,
'expense_date' => isset( $data['expense.date'] ) ? date( 'Y-m-d', strtotime( $data['expense.date'] ) ) : null, 'date' => isset( $data['expense.date'] ) ? date( 'Y-m-d', strtotime( $data['expense.date'] ) ) : null,
'public_notes' => $this->getString( $data, 'expense.public_notes' ), 'public_notes' => $this->getString( $data, 'expense.public_notes' ),
'private_notes' => $this->getString( $data, 'expense.private_notes' ), 'private_notes' => $this->getString( $data, 'expense.private_notes' ),
'expense_category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null, 'expense_category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null,

View File

@ -77,8 +77,14 @@ class CreateAccount
$sp794f3f->key = Str::random(32); $sp794f3f->key = Str::random(32);
} }
$sp794f3f->save(); if(Ninja::isHosted())
{
$sp794f3f->trial_started = now();
$sp794f3f->trial_plan = 'pro';
// $sp794f3f->plan = 'pro';
$sp794f3f->save();
}
$sp035a66 = CreateCompany::dispatchNow($this->request, $sp794f3f); $sp035a66 = CreateCompany::dispatchNow($this->request, $sp794f3f);
$sp035a66->load('account'); $sp035a66->load('account');
$sp794f3f->default_company_id = $sp035a66->id; $sp794f3f->default_company_id = $sp035a66->id;

View File

@ -48,6 +48,9 @@ class CreateCompanyTaskStatuses
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
if(TaskStatus::where('company_id', $this->company->id)->count() > 0)
return;
$task_statuses = [ $task_statuses = [
['name' => ctrans('texts.backlog'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 1], ['name' => ctrans('texts.backlog'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 1],
['name' => ctrans('texts.ready_to_do'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 2], ['name' => ctrans('texts.ready_to_do'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now(), 'status_order' => 2],

View File

@ -131,8 +131,8 @@ class CreateEntityPdf implements ShouldQueue
$entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey($this->entity->client->getSetting($entity_design_id)); $entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey($this->entity->client->getSetting($entity_design_id));
if(!$this->company->account->hasFeature(Account::FEATURE_DIFFERENT_DESIGNS)) // if(!$this->company->account->hasFeature(Account::FEATURE_DIFFERENT_DESIGNS))
$entity_design_id = 2; // $entity_design_id = 2;
$design = Design::find($entity_design_id); $design = Design::find($entity_design_id);

View File

@ -58,7 +58,7 @@ class CreateUser
{ {
$user = new User(); $user = new User();
$user->account_id = $this->account->id; $user->account_id = $this->account->id;
$user->password = bcrypt($this->request['password']); $user->password = $this->request['password'] ? bcrypt($this->request['password']) : '';
$user->accepted_terms_version = config('ninja.terms_version'); $user->accepted_terms_version = config('ninja.terms_version');
$user->confirmation_code = $this->createDbHash(config('database.default')); $user->confirmation_code = $this->createDbHash(config('database.default'));
$user->fill($this->request); $user->fill($this->request);

View File

@ -192,6 +192,8 @@ class Import implements ShouldQueue
nlog("Starting Migration"); nlog("Starting Migration");
nlog($this->user->email); nlog($this->user->email);
info("Starting Migration");
info($this->user->email);
auth()->login($this->user, false); auth()->login($this->user, false);
auth()->user()->setCompany($this->company); auth()->user()->setCompany($this->company);
@ -317,6 +319,12 @@ class Import implements ShouldQueue
$account = $this->company->account; $account = $this->company->account;
$account->fill($data); $account->fill($data);
$account->save(); $account->save();
//Prevent hosted users being pushed into a trial
if(Ninja::isHosted() && $account->plan != ''){
$account->trial_plan = '';
$account->save();
}
} }
/** /**
@ -422,6 +430,7 @@ class Import implements ShouldQueue
private function transformCompanyData(array $data): array private function transformCompanyData(array $data): array
{ {
$company_settings = CompanySettings::defaults(); $company_settings = CompanySettings::defaults();
if (array_key_exists('settings', $data)) { if (array_key_exists('settings', $data)) {
@ -444,6 +453,7 @@ class Import implements ShouldQueue
$data['settings'] = $company_settings; $data['settings'] = $company_settings;
} }
return $data; return $data;
} }
@ -524,7 +534,7 @@ class Import implements ShouldQueue
$user = $user_repository->save($modified, $this->fetchUser($resource['email']), true, true); $user = $user_repository->save($modified, $this->fetchUser($resource['email']), true, true);
$user->email_verified_at = now(); $user->email_verified_at = now();
$user->confirmation_code = ''; // $user->confirmation_code = '';
if($modified['deleted_at']) if($modified['deleted_at'])
$user->deleted_at = now(); $user->deleted_at = now();
@ -556,13 +566,13 @@ class Import implements ShouldQueue
{ {
$value = trim($value); $value = trim($value);
$model_query = (new $model()) $model_query = $model::where($column, $value)
->query() ->where('company_id', $this->company->id)
->where($column, $value) ->withTrashed()
->exists(); ->exists();
if($model_query) if($model_query)
return $value.'_'. Str::random(5); return $value . '_' . Str::random(5);
return $value; return $value;
} }
@ -1099,10 +1109,8 @@ class Import implements ShouldQueue
$modified['client_id'] = $this->transformId('clients', $resource['client_id']); $modified['client_id'] = $this->transformId('clients', $resource['client_id']);
$modified['user_id'] = $this->processUserId($resource); $modified['user_id'] = $this->processUserId($resource);
//$modified['invoice_id'] = $this->transformId('invoices', $resource['invoice_id']);
$modified['company_id'] = $this->company->id; $modified['company_id'] = $this->company->id;
//unset($modified['invoices']);
unset($modified['invoice_id']); unset($modified['invoice_id']);
if (isset($modified['invoices'])) { if (isset($modified['invoices'])) {
@ -1111,8 +1119,8 @@ class Import implements ShouldQueue
$modified['invoices'][$key]['invoice_id'] = $this->transformId('invoices', $invoice['invoice_id']); $modified['invoices'][$key]['invoice_id'] = $this->transformId('invoices', $invoice['invoice_id']);
} else { } else {
nlog($modified['invoices']); nlog($modified['invoices']);
// $modified['credits'][$key]['credit_id'] = $this->transformId('credits', $invoice['invoice_id']); unset($modified['invoices']);
// $modified['credits'][$key]['amount'] = $modified['invoices'][$key]['amount']; //if the transformation didn't work - you _must_ unset this data as it will be incorrect!
} }
} }
} }
@ -1239,7 +1247,8 @@ class Import implements ShouldQueue
$try_quote = false; $try_quote = false;
$exception = false; $exception = false;
$entity = false;
try{ try{
$invoice_id = $this->transformId('invoices', $resource['invoice_id']); $invoice_id = $this->transformId('invoices', $resource['invoice_id']);
$entity = Invoice::where('id', $invoice_id)->withTrashed()->first(); $entity = Invoice::where('id', $invoice_id)->withTrashed()->first();

View File

@ -0,0 +1,76 @@
<?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\Jobs\Util;
use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Util\UnlinkFile;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\Company;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\QuoteInvitation;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class RefreshPdfs implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $company;
public function __construct(Company $company)
{
$this->company = $company;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
MultiDB::setDb($this->company->db);
InvoiceInvitation::where('company_id', $this->company->id)->cursor()->each(function ($invitation) {
nlog("generating invoice pdf for {$invitation->invoice_id}");
CreateEntityPdf::dispatch($invitation);
});
QuoteInvitation::where('company_id', $this->company->id)->cursor()->each(function ($invitation) {
nlog("generating quote pdf for {$invitation->quote_id}");
CreateEntityPdf::dispatch($invitation);
});
CreditInvitation::where('company_id', $this->company->id)->cursor()->each(function ($invitation) {
nlog("generating credit pdf for {$invitation->credit_id}");
CreateEntityPdf::dispatch($invitation);
});
}
}

View File

@ -18,6 +18,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Carbon\Carbon;
class VersionCheck implements ShouldQueue class VersionCheck implements ShouldQueue
{ {
@ -49,7 +50,7 @@ class VersionCheck implements ShouldQueue
if(!$account) if(!$account)
return; return;
if($account->plan == 'white_label' && $account->plan_expires && $account->plan_expires->lt(now())){ if($account->plan == 'white_label' && $account->plan_expires && Carbon::parse($account->plan_expires)->lt(now())){
$account->plan = null; $account->plan = null;
$account->plan_expires = null; $account->plan_expires = null;
$account->save(); $account->save();

View File

@ -59,7 +59,7 @@ class MultiDB
$current_db = config('database.default'); $current_db = config('database.default');
foreach (self::$dbs as $db) { foreach (self::$dbs as $db) {
if (Company::on($db)->whereSubdomain($subdomain)->get()->count() >= 1) { if (Company::on($db)->whereSubdomain($subdomain)->exists()) {
self::setDb($current_db); self::setDb($current_db);
return false; return false;
} }
@ -73,12 +73,12 @@ class MultiDB
public static function checkUserEmailExists($email) : bool public static function checkUserEmailExists($email) : bool
{ {
if (! config('ninja.db.multi_db_enabled')) if (! config('ninja.db.multi_db_enabled'))
return User::where(['email' => $email])->get()->count() >= 1 ?? false; // true >= 1 emails found / false -> == emails found return User::where(['email' => $email])->exists(); // true >= 1 emails found / false -> == emails found
$current_db = config('database.default'); $current_db = config('database.default');
foreach (self::$dbs as $db) { foreach (self::$dbs as $db) {
if (User::on($db)->where(['email' => $email])->get()->count() >= 1) { // if user already exists, validation will fail if (User::on($db)->where(['email' => $email])->exists()) { // if user already exists, validation will fail
self::setDb($current_db); self::setDb($current_db);
return true; return true;
} }
@ -196,7 +196,7 @@ class MultiDB
//multi-db active //multi-db active
foreach (self::$dbs as $db) { foreach (self::$dbs as $db) {
if (User::on($db)->where('email', $email)->count() >= 1){ if (User::on($db)->where('email', $email)->exists()){
self::setDb($db); self::setDb($db);
return true; return true;
} }
@ -214,7 +214,7 @@ class MultiDB
//multi-db active //multi-db active
foreach (self::$dbs as $db) { foreach (self::$dbs as $db) {
if (Document::on($db)->where('hash', $hash)->count() >= 1){ if (Document::on($db)->where('hash', $hash)->exists()){
self::setDb($db); self::setDb($db);
return true; return true;
} }

View File

@ -48,7 +48,7 @@ class PaymentUpdatedActivity implements ShouldQueue
$fields = new stdClass; $fields = new stdClass;
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->payment->user_id; $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->payment->user_id;
$fields->payment_id = $payment->id; $fields->payment_id = $payment->id;
$fields->client_id = $payment->client_id; $fields->client_id = $payment->client_id;

View File

@ -39,6 +39,7 @@ class InvoicePaidActivity implements ShouldQueue
*/ */
public function handle($event) public function handle($event)
{ {
MultiDB::setDb($event->company->db); MultiDB::setDb($event->company->db);
$fields = new stdClass; $fields = new stdClass;
@ -51,6 +52,12 @@ class InvoicePaidActivity implements ShouldQueue
$this->activity_repo->save($fields, $event->invoice, $event->event_vars); $this->activity_repo->save($fields, $event->invoice, $event->event_vars);
if($event->invoice->subscription()->exists())
{
nlog("subscription exists");
$event->invoice->subscription->service()->planPaid($event->invoice);
}
try { try {
$event->invoice->service()->touchPdf(); $event->invoice->service()->touchPdf();
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -71,7 +71,6 @@ class PaymentNotification implements ShouldQueue
} }
/*Google Analytics Track Revenue*/ /*Google Analytics Track Revenue*/
if (isset($payment->company->google_analytics_key)) { if (isset($payment->company->google_analytics_key)) {
$this->trackRevenue($event); $this->trackRevenue($event);

View File

@ -91,7 +91,7 @@ class EntityPaidObject
'texts.notification_payment_paid_subject', 'texts.notification_payment_paid_subject',
['client' => $this->payment->client->present()->name()] ['client' => $this->payment->client->present()->name()]
), ),
'message' => ctrans( 'content' => ctrans(
'texts.notification_payment_paid', 'texts.notification_payment_paid',
['amount' => $amount, ['amount' => $amount,
'client' => $this->payment->client->present()->name(), 'client' => $this->payment->client->present()->name(),

View File

@ -52,17 +52,21 @@ class SupportMessageSent extends Mailable
$account = auth()->user()->account; $account = auth()->user()->account;
$plan = $account->plan ?: 'Forever Free'; $priority = '';
$plan = $account->plan ?: '';
if(strlen($plan) >1)
$priority = '[PRIORITY] ';
$company = auth()->user()->company(); $company = auth()->user()->company();
$user = auth()->user(); $user = auth()->user();
if(Ninja::isHosted()) if(Ninja::isHosted())
$subject = "Hosted {$user->present()->name} - [{$plan} - {$company->db}]"; $subject = "{$priority}Hosted-{$company->db} :: Customer Support - [{$plan}] ".date('M jS, g:ia');
else else
$subject = "Self Host {$user->present()->name} - [{$plan} - {$company->db}]"; $subject = "{$priority}Self Hosted :: Customer Support - [{$plan}] ".date('M jS, g:ia');
return $this->from(config('mail.from.address'), config('mail.from.name')) return $this->from(config('mail.from.address'), $user->present()->name())
->replyTo($user->email, $user->present()->name()) ->replyTo($user->email, $user->present()->name())
->subject($subject) ->subject($subject)
->view('email.support.message', [ ->view('email.support.message', [

View File

@ -54,8 +54,8 @@ class Account extends BaseModel
'deleted_at', 'deleted_at',
'promo_expires', 'promo_expires',
'discount_expires', 'discount_expires',
'trial_started', // 'trial_started',
'plan_expires' // 'plan_expires'
]; ];
const PLAN_FREE = 'free'; const PLAN_FREE = 'free';
@ -120,6 +120,11 @@ class Account extends BaseModel
return $this->hasMany(CompanyUser::class); return $this->hasMany(CompanyUser::class);
} }
public function owner()
{
return $this->hasMany(CompanyUser::class)->where('is_owner', true)->first() ? $this->hasMany(CompanyUser::class)->where('is_owner', true)->first()->user : false;
}
public function getPlan() public function getPlan()
{ {
return $this->plan ?: ''; return $this->plan ?: '';
@ -256,7 +261,7 @@ class Account extends BaseModel
if ($trial_plan && $include_trial) { if ($trial_plan && $include_trial) {
$trial_started = $this->trial_started; $trial_started = $this->trial_started;
$trial_expires = $this->trial_started->addSeconds($this->trial_duration); $trial_expires = Carbon::parse($this->trial_started)->addSeconds($this->trial_duration);
if($trial_expires->greaterThan(now())){ if($trial_expires->greaterThan(now())){
$trial_active = true; $trial_active = true;

View File

@ -16,6 +16,7 @@ use App\Models\Presenters\CompanyPresenter;
use App\Models\User; use App\Models\User;
use App\Services\Notification\NotificationService; use App\Services\Notification\NotificationService;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\CompanySettingsSaver; use App\Utils\Traits\CompanySettingsSaver;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\ThrottlesEmail; use App\Utils\Traits\ThrottlesEmail;
@ -31,6 +32,7 @@ class Company extends BaseModel
use MakesHash; use MakesHash;
use CompanySettingsSaver; use CompanySettingsSaver;
use ThrottlesEmail; use ThrottlesEmail;
use AppSetup;
const ENTITY_RECURRING_INVOICE = 'recurring_invoice'; const ENTITY_RECURRING_INVOICE = 'recurring_invoice';
const ENTITY_CREDIT = 'credit'; const ENTITY_CREDIT = 'credit';
@ -311,7 +313,17 @@ class Company extends BaseModel
public function timezone() public function timezone()
{ {
return Timezone::find($this->settings->timezone_id);
$timezones = Cache::get('timezones');
if(!$timezones)
$this->buildCache(true);
return $timezones->filter(function ($item) {
return $item->id == $this->settings->timezone_id;
})->first();
// return Timezone::find($this->settings->timezone_id);
} }
public function designs() public function designs()
@ -339,7 +351,18 @@ class Company extends BaseModel
*/ */
public function language() public function language()
{ {
return Language::find($this->settings->language_id);
$languages = Cache::get('languages');
if(!$languages)
$this->buildCache(true);
return $languages->filter(function ($item) {
return $item->id == $this->settings->language_id;
})->first();
// return Language::find($this->settings->language_id);
} }
public function getLocale() public function getLocale()

View File

@ -371,6 +371,11 @@ class CompanyGateway extends BaseModel
return $fee; return $fee;
} }
public function webhookUrl()
{
return route('payment_webhook', ['company_key' => $this->company->company_key, 'company_gateway_id' => $this->hashed_id]);
}
/** /**
* we need to average out the gateway fees across all the invoices * we need to average out the gateway fees across all the invoices
* so lets iterate. * so lets iterate.
@ -412,4 +417,6 @@ class CompanyGateway extends BaseModel
return $this return $this
->where('id', $this->decodePrimaryKey($value))->firstOrFail(); ->where('id', $this->decodePrimaryKey($value))->firstOrFail();
} }
} }

View File

@ -93,6 +93,7 @@ class Invoice extends BaseModel
'exchange_rate', 'exchange_rate',
'subscription_id', 'subscription_id',
'auto_bill_enabled', 'auto_bill_enabled',
'uses_inclusive_taxes',
]; ];
protected $casts = [ protected $casts = [
@ -165,7 +166,7 @@ class Invoice extends BaseModel
public function recurring_invoice() public function recurring_invoice()
{ {
return $this->belongsTo(RecurringInvoice::class)->withTrashed(); return $this->belongsTo(RecurringInvoice::class, 'recurring_id', 'id')->withTrashed();
} }
public function assigned_user() public function assigned_user()

View File

@ -29,6 +29,7 @@ class PaymentType extends StaticModel
const NOVA = 11; const NOVA = 11;
const CREDIT_CARD_OTHER = 12; const CREDIT_CARD_OTHER = 12;
const PAYPAL = 13; const PAYPAL = 13;
const CHECK = 15;
const CARTE_BLANCHE = 16; const CARTE_BLANCHE = 16;
const UNIONPAY = 17; const UNIONPAY = 17;
const JCB = 18; const JCB = 18;

View File

@ -21,6 +21,10 @@ class UserPresenter extends EntityPresenter
*/ */
public function name() public function name()
{ {
if(!$this->entity)
return "No User Object Available";
$first_name = isset($this->entity->first_name) ? $this->entity->first_name : ''; $first_name = isset($this->entity->first_name) ? $this->entity->first_name : '';
$last_name = isset($this->entity->last_name) ? $this->entity->last_name : ''; $last_name = isset($this->entity->last_name) ? $this->entity->last_name : '';

View File

@ -67,6 +67,7 @@ class SystemLog extends Model
const TYPE_CUSTOM = 306; const TYPE_CUSTOM = 306;
const TYPE_BRAINTREE = 307; const TYPE_BRAINTREE = 307;
const TYPE_WEPAY = 309; const TYPE_WEPAY = 309;
const TYPE_PAYFAST = 310;
const TYPE_QUOTA_EXCEEDED = 400; const TYPE_QUOTA_EXCEEDED = 400;

View File

@ -161,7 +161,7 @@ class User extends Authenticatable implements MustVerifyEmail
public function setCompany($company) public function setCompany($company)
{ {
$this->company = $company; $this->company = $company;
return $this; return $this;
} }

View File

@ -548,6 +548,15 @@ class BaseDriver extends AbstractPaymentDriver
); );
} }
public function genericWebhookUrl()
{
return route('payment_notification_webhook', [
'company_key' => $this->client->company->company_key,
'company_gateway_id' => $this->encodePrimaryKey($this->company_gateway->id),
'client' => $this->encodePrimaryKey($this->client->id),
]);
}
/* Performs an extra iterate on the gatewayTypes() array and passes back only the enabled gateways*/ /* Performs an extra iterate on the gatewayTypes() array and passes back only the enabled gateways*/
public function gatewayTypeEnabled($type) public function gatewayTypeEnabled($type)
{ {

View File

@ -11,6 +11,7 @@
namespace App\PaymentDrivers; namespace App\PaymentDrivers;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Payment; use App\Models\Payment;
@ -39,6 +40,22 @@ class DriverTemplate extends BaseDriver
const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model
public function init()
{
return $this; /* This is where you boot the gateway with your auth credentials*/
}
/* Returns an array of gateway types for the payment gateway */
public function gatewayTypes(): array
{
$types = [];
$types[] = GatewayType::CREDIT_CARD;
return $types;
}
/* Sets the payment method initialized */
public function setPaymentMethod($payment_method_id) public function setPaymentMethod($payment_method_id)
{ {
$class = self::$methods[$payment_method_id]; $class = self::$methods[$payment_method_id];
@ -75,4 +92,8 @@ class DriverTemplate extends BaseDriver
{ {
return $this->payment_method->yourTokenBillingImplmentation(); //this is your custom implementation from here return $this->payment_method->yourTokenBillingImplmentation(); //this is your custom implementation from here
} }
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)
{
}
} }

View File

@ -0,0 +1,282 @@
<?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\PaymentDrivers\PayFast;
use App\Exceptions\PaymentFailed;
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\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\PayFastPaymentDriver;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class CreditCard
{
public $payfast;
public function __construct(PayFastPaymentDriver $payfast)
{
$this->payfast = $payfast;
}
/*
$data = array();
$data['merchant_id'] = $this->getMerchantId();
$data['merchant_key'] = $this->getMerchantKey();
$data['return_url'] = $this->getReturnUrl();
$data['cancel_url'] = $this->getCancelUrl();
$data['notify_url'] = $this->getNotifyUrl();
if ($this->getCard()) {
$data['name_first'] = $this->getCard()->getFirstName();
$data['name_last'] = $this->getCard()->getLastName();
$data['email_address'] = $this->getCard()->getEmail();
}
$data['m_payment_id'] = $this->getTransactionId();
$data['amount'] = $this->getAmount();
$data['item_name'] = $this->getDescription();
$data['custom_int1'] = $this->getCustomInt1();
$data['custom_int2'] = $this->getCustomInt2();
$data['custom_int3'] = $this->getCustomInt3();
$data['custom_int4'] = $this->getCustomInt4();
$data['custom_int5'] = $this->getCustomInt5();
$data['custom_str1'] = $this->getCustomStr1();
$data['custom_str2'] = $this->getCustomStr2();
$data['custom_str3'] = $this->getCustomStr3();
$data['custom_str4'] = $this->getCustomStr4();
$data['custom_str5'] = $this->getCustomStr5();
if ($this->getPaymentMethod()) {
$data['payment_method'] = $this->getPaymentMethod();
}
if (1 == $this->getSubscriptionType()) {
$data['subscription_type'] = $this->getSubscriptionType();
$data['billing_date'] = $this->getBillingDate();
$data['recurring_amount'] = $this->getRecurringAmount();
$data['frequency'] = $this->getFrequency();
$data['cycles'] = $this->getCycles();
}
if (2 == $this->getSubscriptionType()) {
$data['subscription_type'] = $this->getSubscriptionType();
}
$data['passphrase'] = $this->getParameter('passphrase'); 123456789012aV
$data['signature'] = $this->generateSignature($data);
*/
public function authorizeView($data)
{
$hash = Str::random(32);
Cache::put($hash, 'cc_auth', 300);
$data = [
'merchant_id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'merchant_key' => $this->payfast->company_gateway->getConfigField('merchantKey'),
'return_url' => route('client.payment_methods.index'),
'cancel_url' => route('client.payment_methods.index'),
'notify_url' => $this->payfast->genericWebhookUrl(),
'm_payment_id' => $hash,
'amount' => 5,
'item_name' => 'pre-auth',
'item_description' => 'Credit Card Pre Authorization',
'subscription_type' => 2,
'passphrase' => $this->payfast->company_gateway->getConfigField('passphrase'),
];
$data['signature'] = $this->payfast->generateSignature($data);
$data['gateway'] = $this->payfast;
$data['payment_endpoint_url'] = $this->payfast->endpointUrl();
return render('gateways.payfast.authorize', $data);
}
/*
'm_payment_id' => NULL,
'pf_payment_id' => '1409993',
'payment_status' => 'COMPLETE',
'item_name' => 'pre-auth',
'item_description' => NULL,
'amount_gross' => '5.00',
'amount_fee' => '-2.53',
'amount_net' => '2.47',
'custom_str1' => NULL,
'custom_str2' => NULL,
'custom_str3' => NULL,
'custom_str4' => NULL,
'custom_str5' => NULL,
'custom_int1' => NULL,
'custom_int2' => NULL,
'custom_int3' => NULL,
'custom_int4' => NULL,
'custom_int5' => NULL,
'name_first' => NULL,
'name_last' => NULL,
'email_address' => NULL,
'merchant_id' => '10023100',
'token' => '34b66bc2-3c54-9590-03ea-42ee8b89922a',
'billing_date' => '2021-07-05',
'signature' => 'ebdb4ca937d0e3f43462841c0afc6ad9',
'q' => '/payment_notification_webhook/EhbnVYyzJZyccY85hcHIkIzNPI2rtHzznAyyyG73oSxZidAdN9gf8BvAKDomqeHp/4openRe7Az/WPe99p3eLy',
*/
public function authorizeResponse($request)
{
$data = $request->all();
$cgt = [];
$cgt['token'] = $data['token'];
$cgt['payment_method_id'] = GatewayType::CREDIT_CARD;
$payment_meta = new \stdClass;
$payment_meta->exp_month = 'xx';
$payment_meta->exp_year = 'xx';
$payment_meta->brand = 'CC';
$payment_meta->last4 = 'xxxx';
$payment_meta->type = GatewayType::CREDIT_CARD;
$cgt['payment_meta'] = $payment_meta;
$token = $this->payfast->storeGatewayToken($cgt, []);
return response()->json([], 200);
}
public function paymentView($data)
{
$payfast_data = [
'merchant_id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'merchant_key' => $this->payfast->company_gateway->getConfigField('merchantKey'),
'return_url' => route('client.payments.index'),
'cancel_url' => route('client.payment_methods.index'),
'notify_url' => $this->payfast->genericWebhookUrl(),
'm_payment_id' => $data['payment_hash'],
'amount' => $data['amount_with_fee'],
'item_name' => 'purchase',
'item_description' => ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number'),
'passphrase' => $this->payfast->company_gateway->getConfigField('passphrase'),
];
$payfast_data['signature'] = $this->payfast->generateSignature($payfast_data);
$payfast_data['gateway'] = $this->payfast;
$payfast_data['payment_endpoint_url'] = $this->payfast->endpointUrl();
return render('gateways.payfast.pay', array_merge($data, $payfast_data));
}
/*
[2021-07-05 11:21:24] local.INFO: array (
'm_payment_id' => 'B7G9Q2vPhqkLEoMwwY1paXvPGuFxpbDe',
'pf_payment_id' => '1410364',
'payment_status' => 'COMPLETE',
'item_name' => 'purchase',
'item_description' => 'Invoices: ["0001"]',
'amount_gross' => '100.00',
'amount_fee' => '-2.30',
'amount_net' => '97.70',
'custom_str1' => NULL,
'custom_str2' => NULL,
'custom_str3' => NULL,
'custom_str4' => NULL,
'custom_str5' => NULL,
'custom_int1' => NULL,
'custom_int2' => NULL,
'custom_int3' => NULL,
'custom_int4' => NULL,
'custom_int5' => NULL,
'name_first' => NULL,
'name_last' => NULL,
'email_address' => NULL,
'merchant_id' => '10023100',
'signature' => '3ed27638479fd65cdffb0f4910679d10',
'q' => '/payment_notification_webhook/EhbnVYyzJZyccY85hcHIkIzNPI2rtHzznAyyyG73oSxZidAdN9gf8BvAKDomqeHp/4openRe7Az/WPe99p3eLy',
)
*/
public function paymentResponse(Request $request)
{
$response_array = $request->all();
$state = [
'server_response' => $request->all(),
'payment_hash' => $request->input('m_payment_id'),
];
$this->payfast->payment_hash->data = array_merge((array) $this->payfast->payment_hash->data, $state);
$this->payfast->payment_hash->save();
if($response_array['payment_status'] == 'COMPLETE') {
$this->payfast->logSuccessfulGatewayResponse(['response' => $response_array, 'data' => $this->payfast->payment_hash], SystemLog::TYPE_PAYFAST);
return $this->processSuccessfulPayment($response_array);
}
else {
$this->processUnsuccessfulPayment($response_array);
}
}
private function processSuccessfulPayment($response_array)
{
$payment_record = [];
$payment_record['amount'] = $response_array['amount_gross'];
$payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER;
$payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD;
$payment_record['transaction_reference'] = $response_array['pf_payment_id'];
$payment = $this->payfast->createPayment($payment_record, Payment::STATUS_COMPLETED);
return redirect()->route('client.payments.show', ['payment' => $this->payfast->encodePrimaryKey($payment->id)]);
}
private function processUnsuccessfulPayment($server_response)
{
PaymentFailureMailer::dispatch($this->payfast->client, $server_response->cancellation_reason, $this->payfast->client->company, $server_response->amount);
PaymentFailureMailer::dispatch(
$this->payfast->client,
$server_response,
$this->payfast->client->company,
$server_response['amount_gross']
);
$message = [
'server_response' => $server_response,
'data' => $this->payfast->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYFAST,
$this->payfast->client,
$this->payfast->client->company,
);
throw new PaymentFailed('Failed to process the payment.', 500);
}
}

View File

@ -0,0 +1,184 @@
<?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\PaymentDrivers\PayFast;
use App\Exceptions\PaymentFailed;
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\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\PayFastPaymentDriver;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use GuzzleHttp\RequestOptions;
class Token
{
public $payfast;
//https://api.payfast.co.za/subscriptions/dc0521d3-55fe-269b-fa00-b647310d760f/adhoc
public function __construct(PayFastPaymentDriver $payfast)
{
$this->payfast = $payfast;
}
// Attributes
// merchant-id
// integer, 8 char | REQUIRED
// Header, the Merchant ID as given by the PayFast system.
// version
// string | REQUIRED
// Header, the PayFast API version (i.e. v1).
// timestamp
// ISO-8601 date and time | REQUIRED
// Header, the current timestamp (YYYY-MM-DDTHH:MM:SS[+HH:MM]).
// signature
// string | REQUIRED
// Header, MD5 hash of the alphabetised submitted header and body variables, as well as the passphrase. Characters must be in lower case.
// amount
// integer | REQUIRED
// Body, the amount which the buyer must pay, in cents (ZAR), no decimals.
// item_name
// string, 100 char | REQUIRED
// Body, the name of the item being charged for.
// item_description
// string, 255 char | OPTIONAL
// Body, the description of the item being charged for.
// itn
// boolean | OPTIONAL
// Body, specify whether an ITN must be sent for the tokenization payment (true by default).
// m_payment_id
// string, 100 char | OPTIONAL
// Body, unique payment ID on the merchants system.
// cc_cvv
// numeric | OPTIONAL
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$amount = round(($amount * pow(10, $this->payfast->client->currency()->precision)),0);
$header =[
'merchant-id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'timestamp' => now()->format('c'),
'version' => 'v1',
];
nlog($header);
$body = [
'amount' => $amount,
'item_name' => 'purchase',
'item_description' => ctrans('texts.invoices') . ': ' . collect($payment_hash->invoices())->pluck('invoice_number'),
'm_payment_id' => $payment_hash->hash,
'passphrase' => $this->payfast->company_gateway->getConfigField('passphrase'),
];
$header['signature'] = $this->genSig(array_merge($header, $body));
nlog($header['signature']);
nlog($header['timestamp']);
nlog($this->payfast->company_gateway->getConfigField('merchantId'));
$result = $this->send($header, $body, $cgt->token);
nlog($result);
// /*Refactor and push to BaseDriver*/
// if ($data['response'] != null && $data['response']->getMessages()->getResultCode() == 'Ok') {
// $response = $data['response'];
// $this->storePayment($payment_hash, $data);
// $vars = [
// 'invoices' => $payment_hash->invoices(),
// 'amount' => $amount,
// ];
// $logger_message = [
// 'server_response' => $response->getTransactionResponse()->getTransId(),
// 'data' => $this->formatGatewayResponse($data, $vars),
// ];
// SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company);
// return true;
// } else {
// $vars = [
// 'invoices' => $payment_hash->invoices(),
// 'amount' => $amount,
// ];
// $logger_message = [
// 'server_response' => $response->getTransactionResponse()->getTransId(),
// 'data' => $this->formatGatewayResponse($data, $vars),
// ];
// PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransactionResponse()->getTransId(), $this->authorize->client->company, $amount);
// SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company);
// return false;
// }
}
private function genSig($data)
{
$fields = [];
ksort($data);
foreach($data as $key => $value)
{
if (!empty($data[$key])) {
$fields[$key] = $data[$key];
}
}
return md5(http_build_query($fields));
}
private function send($headers, $body, $token)
{
$client = new \GuzzleHttp\Client(
[
'headers' => $headers,
]);
try {
$response = $client->post("https://api.payfast.co.za/subscriptions/{$token}/adhoc?testing=true",[
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false
]);
return json_decode($response->getBody(),true);
}
catch(\Exception $e)
{
nlog($e->getMessage());
}
}
}

View File

@ -0,0 +1,202 @@
<?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\PaymentDrivers;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\PaymentDrivers\PayFast\CreditCard;
use App\PaymentDrivers\PayFast\Token;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class PayFastPaymentDriver extends BaseDriver
{
use MakesHash;
public $refundable = false; //does this gateway support refunds?
public $token_billing = false; //does this gateway support token billing?
public $can_authorise_credit_card = true; //does this gateway support authorizations?
public $payfast; //initialized gateway
public $payment_method; //initialized payment method
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYFAST;
//developer resources
//https://sandbox.payfast.co.za/
public function gatewayTypes(): array
{
$types = [];
if($this->client->currency()->code == 'ZAR')
$types[] = GatewayType::CREDIT_CARD;
return $types;
}
public function endpointUrl()
{
if($this->company_gateway->getConfigField('testMode'))
return 'https://sandbox.payfast.co.za/eng/process';
return 'https://www.payfast.co.za/eng/process';
}
public function init()
{
try{
$this->payfast = new \PayFast\PayFastPayment(
[
'merchantId' => $this->company_gateway->getConfigField('merchantId'),
'merchantKey' => $this->company_gateway->getConfigField('merchantKey'),
'passPhrase' => $this->company_gateway->getConfigField('passPhrase'),
'testMode' => $this->company_gateway->getConfigField('testMode')
]
);
} catch(Exception $e) {
echo '##PAYFAST## There was an exception: '.$e->getMessage();
}
return $this;
}
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
public function authorizeView(array $data)
{
return $this->payment_method->authorizeView($data);
}
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request);
}
public function processPaymentView(array $data)
{
return $this->payment_method->paymentView($data); //this is your custom implementation from here
}
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request); //this is your custom implementation from here
}
public function refund(Payment $payment, $amount, $return_client_response = false)
{
return false;
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
return (new Token($this))->tokenBilling($cgt, $payment_hash);
}
public function generateSignature($data)
{
$fields = array();
// specific order required by PayFast
// @see https://developers.payfast.co.za/documentation/#checkout-page
foreach (array('merchant_id', 'merchant_key', 'return_url', 'cancel_url', 'notify_url', 'name_first',
'name_last', 'email_address', 'cell_number',
/**
* Transaction Details
*/
'm_payment_id', 'amount', 'item_name', 'item_description',
/**
* Custom return data
*/
'custom_int1', 'custom_int2', 'custom_int3', 'custom_int4', 'custom_int5',
'custom_str1', 'custom_str2', 'custom_str3', 'custom_str4', 'custom_str5',
/**
* Email confirmation
*/
'email_confirmation', 'confirmation_address',
/**
* Payment Method
*/
'payment_method',
/**
* Subscriptions
*/
'subscription_type', 'billing_date', 'recurring_amount', 'frequency', 'cycles',
/**
* Passphrase for md5 signature generation
*/
'passphrase') as $key) {
if (!empty($data[$key])) {
$fields[$key] = $data[$key];
}
}
return md5(http_build_query($fields));
}
public function processWebhookRequest(Request $request, Payment $payment = null)
{
$data = $request->all();
nlog($data);
if(array_key_exists('m_payment_id', $data))
{
$hash = Cache::get($data['m_payment_id']);
switch ($hash)
{
case 'cc_auth':
return $this->setPaymentMethod(GatewayType::CREDIT_CARD)
->authorizeResponse($request);
break;
default:
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$data['m_payment_id']])->first();
return $this->setPaymentMethod(GatewayType::CREDIT_CARD)
->setPaymentHash($payment_hash)
->processPaymentResponse($request);
break;
}
}
return response()->json([], 200);
}
}

View File

@ -195,7 +195,8 @@ class StripePaymentDriver extends BaseDriver
$fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required'];
} }
$fields[] = ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required']; if($this->company_gateway->require_postal_code)
$fields[] = ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'];
if ($this->company_gateway->require_shipping_address) { if ($this->company_gateway->require_shipping_address) {
$fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required'];

View File

@ -117,7 +117,6 @@ use WePayCommon;
nlog("authorize the card first!"); nlog("authorize the card first!");
$response = $this->wepay_payment_driver->wepay->request('credit_card/authorize', array( $response = $this->wepay_payment_driver->wepay->request('credit_card/authorize', array(
// 'callback_uri' => route('payment_webhook', ['company_key' => $this->wepay_payment_driver->company_gateway->company->company_key, 'company_gateway_id' => $this->wepay_payment_driver->company_gateway->hashed_id]),
'client_id' => config('ninja.wepay.client_id'), 'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'), 'client_secret' => config('ninja.wepay.client_secret'),
'credit_card_id' => (int)$request->input('credit_card_id'), 'credit_card_id' => (int)$request->input('credit_card_id'),

View File

@ -14,6 +14,7 @@ namespace App\PaymentDrivers\WePay;
use App\Exceptions\PaymentFailed; use App\Exceptions\PaymentFailed;
use App\Jobs\Mail\PaymentFailureMailer; use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;

View File

@ -58,8 +58,13 @@ class ClientRepository extends BaseRepository
return $client; return $client;
} }
$client->fill($data); if(!$client->id && auth()->user() && auth()->user()->company() && (!array_key_exists('country_id', $data) || empty($data['country_id']))){
$data['country_id'] = auth()->user()->company()->settings->country_id;
}
$client->fill($data);
$client->save();
if (!isset($client->number) || empty($client->number)) { if (!isset($client->number) || empty($client->number)) {
$client->number = $this->getNextClientNumber($client); $client->number = $this->getNextClientNumber($client);
} }

View File

@ -80,6 +80,11 @@ class PaymentRepository extends BaseRepository {
$client->service()->updatePaidToDate($data['amount'])->save(); $client->service()->updatePaidToDate($data['amount'])->save();
} }
elseif($data['amount'] >0){
//this fixes an edge case with unapplied payments
$client->service()->updatePaidToDate($data['amount'])->save();
}
if (array_key_exists('credits', $data) && is_array($data['credits']) && count($data['credits']) > 0) { if (array_key_exists('credits', $data) && is_array($data['credits']) && count($data['credits']) > 0) {
$_credit_totals = array_sum(array_column($data['credits'], 'amount')); $_credit_totals = array_sum(array_column($data['credits'], 'amount'));

View File

@ -104,6 +104,8 @@ class ApplyPayment extends AbstractService
->ledger() ->ledger()
->updatePaymentBalance($amount_paid); ->updatePaymentBalance($amount_paid);
nlog("updating client balance by amount {$amount_paid}");
$this->invoice $this->invoice
->client ->client
->service() ->service()

View File

@ -71,7 +71,8 @@ class HandleReversal extends AbstractService
$credit = CreditFactory::create($this->invoice->company_id, $this->invoice->user_id); $credit = CreditFactory::create($this->invoice->company_id, $this->invoice->user_id);
$credit->client_id = $this->invoice->client_id; $credit->client_id = $this->invoice->client_id;
$credit->invoice_id = $this->invoice->id; $credit->invoice_id = $this->invoice->id;
$credit->date = now();
$item = InvoiceItemFactory::create(); $item = InvoiceItemFactory::create();
$item->quantity = 1; $item->quantity = 1;
$item->cost = (float) $total_paid; $item->cost = (float) $total_paid;

View File

@ -33,13 +33,9 @@ class InvoiceService
private $invoice; private $invoice;
protected $client_service;
public function __construct($invoice) public function __construct($invoice)
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
$this->client_service = new ClientService($invoice->client);
} }
/** /**
@ -49,7 +45,7 @@ class InvoiceService
*/ */
public function markPaid() public function markPaid()
{ {
$this->invoice = (new MarkPaid($this->client_service, $this->invoice))->run(); $this->invoice = (new MarkPaid($this->invoice))->run();
return $this; return $this;
} }

View File

@ -29,14 +29,10 @@ class MarkPaid extends AbstractService
{ {
use GeneratesCounter; use GeneratesCounter;
private $client_service;
private $invoice; private $invoice;
public function __construct(ClientService $client_service, Invoice $invoice) public function __construct(Invoice $invoice)
{ {
$this->client_service = $client_service;
$this->invoice = $invoice; $this->invoice = $invoice;
} }
@ -92,7 +88,9 @@ class MarkPaid extends AbstractService
$payment->ledger() $payment->ledger()
->updatePaymentBalance($payment->amount * -1); ->updatePaymentBalance($payment->amount * -1);
$this->client_service $this->invoice
->client
->service()
->updateBalance($payment->amount * -1) ->updateBalance($payment->amount * -1)
->updatePaidToDate($payment->amount) ->updatePaidToDate($payment->amount)
->save(); ->save();

View File

@ -77,7 +77,8 @@ class SubscriptionService
$recurring_invoice->next_send_date = now(); $recurring_invoice->next_send_date = now();
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice); $recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->auto_bill = $this->subscription->auto_bill;
/* Start the recurring service */ /* Start the recurring service */
$recurring_invoice->service() $recurring_invoice->service()
->start() ->start()
@ -96,8 +97,6 @@ class SubscriptionService
$response = $this->triggerWebhook($context); $response = $this->triggerWebhook($context);
// nlog($response);
$this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
} }
@ -387,6 +386,7 @@ class SubscriptionService
$pro_rata_charge_amount = 0; $pro_rata_charge_amount = 0;
$pro_rata_refund_amount = 0; $pro_rata_refund_amount = 0;
$is_credit = false;
$last_invoice = Invoice::where('subscription_id', $recurring_invoice->subscription_id) $last_invoice = Invoice::where('subscription_id', $recurring_invoice->subscription_id)
->where('client_id', $recurring_invoice->client_id) ->where('client_id', $recurring_invoice->client_id)
@ -395,7 +395,22 @@ class SubscriptionService
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->first(); ->first();
if($last_invoice->balance > 0) if(!$last_invoice){
$is_credit = true;
$last_invoice = Credit::where('subscription_id', $recurring_invoice->subscription_id)
->where('client_id', $recurring_invoice->client_id)
->where('is_deleted', 0)
->withTrashed()
->orderBy('id', 'desc')
->first();
$pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription);
}
elseif($last_invoice->balance > 0)
{ {
$pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription);
nlog("pro rata charge = {$pro_rata_charge_amount}"); nlog("pro rata charge = {$pro_rata_charge_amount}");
@ -410,7 +425,7 @@ class SubscriptionService
nlog("total payable = {$total_payable}"); nlog("total payable = {$total_payable}");
$credit = $this->createCredit($last_invoice, $target_subscription); $credit = $this->createCredit($last_invoice, $target_subscription, $is_credit);
$new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice); $new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice);
@ -510,7 +525,7 @@ class SubscriptionService
$total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price; $total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price;
return $this->proRataInvoice($last_invoice, $target_subscription); return $this->proRataInvoice($last_invoice, $target_subscription, $recurring_invoice->client_id);
} }
@ -522,26 +537,27 @@ class SubscriptionService
*/ */
private function handlePlanChange($payment_hash) private function handlePlanChange($payment_hash)
{ {
nlog("handle plan change"); nlog("handle plan change");
$old_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice); $old_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice);
$recurring_invoice = $this->createNewRecurringInvoice($old_recurring_invoice); $recurring_invoice = $this->createNewRecurringInvoice($old_recurring_invoice);
$context = [ $context = [
'context' => 'change_plan', 'context' => 'change_plan',
'recurring_invoice' => $recurring_invoice->hashed_id, 'recurring_invoice' => $recurring_invoice->hashed_id,
'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id), 'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id),
'client' => $recurring_invoice->client->hashed_id, 'client' => $recurring_invoice->client->hashed_id,
'subscription' => $this->subscription->hashed_id, 'subscription' => $this->subscription->hashed_id,
'contact' => auth('contact')->user()->hashed_id, 'contact' => auth('contact')->user()->hashed_id,
]; ];
$response = $this->triggerWebhook($context); $response = $this->triggerWebhook($context);
nlog($response); nlog($response);
return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
} }
@ -552,7 +568,7 @@ nlog("handle plan change");
* @param RecurringInvoice $old_recurring_invoice * @param RecurringInvoice $old_recurring_invoice
* @return RecurringInvoice * @return RecurringInvoice
*/ */
private function createNewRecurringInvoice($old_recurring_invoice) :RecurringInvoice public function createNewRecurringInvoice($old_recurring_invoice) :RecurringInvoice
{ {
$old_recurring_invoice->service()->stop()->save(); $old_recurring_invoice->service()->stop()->save();
@ -581,9 +597,11 @@ nlog("handle plan change");
* @param Subscription $target * @param Subscription $target
* @return Credit * @return Credit
*/ */
private function createCredit($last_invoice, $target) private function createCredit($last_invoice, $target, $is_credit = false)
{ {
$last_invoice_is_credit = $is_credit ? false : true;
$subscription_repo = new SubscriptionRepository(); $subscription_repo = new SubscriptionRepository();
$credit_repo = new CreditRepository(); $credit_repo = new CreditRepository();
@ -593,7 +611,7 @@ nlog("handle plan change");
$line_items = $subscription_repo->generateLineItems($target, false, true); $line_items = $subscription_repo->generateLineItems($target, false, true);
$credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, true)); $credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, $last_invoice_is_credit));
$data = [ $data = [
'client_id' => $last_invoice->client_id, 'client_id' => $last_invoice->client_id,
@ -613,19 +631,19 @@ nlog("handle plan change");
* @param Subscription $target * @param Subscription $target
* @return Invoice * @return Invoice
*/ */
private function proRataInvoice($last_invoice, $target) private function proRataInvoice($last_invoice, $target, $client_id)
{ {
$subscription_repo = new SubscriptionRepository(); $subscription_repo = new SubscriptionRepository();
$invoice_repo = new InvoiceRepository(); $invoice_repo = new InvoiceRepository();
$invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); $invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
$invoice->date = now()->format('Y-m-d'); $invoice->date = now()->format('Y-m-d');
$invoice->subscription_id = $this->subscription->id; $invoice->subscription_id = $target->id;
$invoice->line_items = array_merge($subscription_repo->generateLineItems($target), $this->calculateProRataRefundItems($last_invoice)); $invoice->line_items = array_merge($subscription_repo->generateLineItems($target), $this->calculateProRataRefundItems($last_invoice));
$data = [ $data = [
'client_id' => $last_invoice->client_id, 'client_id' => $client_id,
'quantity' => 1, 'quantity' => 1,
'date' => now()->format('Y-m-d'), 'date' => now()->format('Y-m-d'),
]; ];
@ -694,10 +712,14 @@ nlog("handle plan change");
*/ */
public function triggerWebhook($context) public function triggerWebhook($context)
{ {
nlog("trigger webook");
if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) { if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) {
return ["message" => "Success", "status_code" => 200]; return ["message" => "Success", "status_code" => 200];
} }
nlog("past first if");
$response = false; $response = false;
$body = array_merge($context, [ $body = array_merge($context, [
@ -708,6 +730,8 @@ nlog("handle plan change");
$response = $this->sendLoad($this->subscription, $body); $response = $this->sendLoad($this->subscription, $body);
nlog("after response");
/* Append the response to the system logger body */ /* Append the response to the system logger body */
if(is_array($response)){ if(is_array($response)){
@ -731,6 +755,7 @@ nlog("handle plan change");
$client->company, $client->company,
); );
nlog("ready to fire back");
if(is_array($body)) if(is_array($body))
return $response; return $response;
@ -905,4 +930,22 @@ nlog("handle plan change");
return redirect($default_redirect); return redirect($default_redirect);
} }
public function planPaid($invoice)
{
$recurring_invoice_hashed_id = $invoice->recurring_invoice()->exists() ? $invoice->recurring_invoice->hashed_id : null;
$context = [
'context' => 'plan_paid',
'subscription' => $this->subscription->hashed_id,
'recurring_invoice' => $recurring_invoice_hashed_id,
'client' => $invoice->client->hashed_id,
'contact' => $invoice->client->primary_contact()->first() ? $invoice->client->contacts->first() : false,
'invoice' => $invoice->hashed_id,
];
$response = $this->triggerWebhook($context);
return true;
}
} }

View File

@ -173,9 +173,15 @@ class HtmlEngine
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')]; $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.balance_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.balance_due')];
} else { } else {
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
if($this->entity->status_id == 1){
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->amount, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.balance_due')];
}
else{
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
}
} }
$data['$quote.balance_due'] = &$data['$balance_due']; $data['$quote.balance_due'] = &$data['$balance_due'];

View File

@ -26,9 +26,9 @@ use App\Utils\CurlUtils;
use App\Utils\HtmlEngine; use App\Utils\HtmlEngine;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class Phantom class Phantom

View File

@ -48,7 +48,7 @@ trait AppSetup
$orderBy = 'num_days'; $orderBy = 'num_days';
} elseif ($name == 'fonts') { } elseif ($name == 'fonts') {
$orderBy = 'sort_order'; $orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) { } elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks', 'timezones'])) {
$orderBy = 'name'; $orderBy = 'name';
} else { } else {
$orderBy = 'id'; $orderBy = 'id';

View File

@ -126,7 +126,7 @@ trait GeneratesCounter
break; break;
case Quote::class: case Quote::class:
if ($this->hasSharedCounter($client)) if ($this->hasSharedCounter($client, 'quote'))
return 'invoice_number_counter'; return 'invoice_number_counter';
return 'quote_number_counter'; return 'quote_number_counter';
@ -138,7 +138,7 @@ trait GeneratesCounter
return 'payment_number_counter'; return 'payment_number_counter';
break; break;
case Credit::class: case Credit::class:
if ($this->hasSharedCounter($client)) if ($this->hasSharedCounter($client, 'credit'))
return 'invoice_number_counter'; return 'invoice_number_counter';
return 'credit_number_counter'; return 'credit_number_counter';
@ -318,9 +318,13 @@ trait GeneratesCounter
* *
* @return bool True if has shared counter, False otherwise. * @return bool True if has shared counter, False otherwise.
*/ */
public function hasSharedCounter(Client $client) : bool public function hasSharedCounter(Client $client, string $type = 'quote') : bool
{ {
return (bool) $client->getSetting('shared_invoice_quote_counter') || (bool) $client->getSetting('shared_invoice_credit_counter'); if($type == 'quote')
return (bool) $client->getSetting('shared_invoice_quote_counter');
if($type == 'credit')
return (bool) $client->getSetting('shared_invoice_credit_counter');
} }
/** /**

View File

@ -64,6 +64,7 @@
"maennchen/zipstream-php": "^1.2", "maennchen/zipstream-php": "^1.2",
"nwidart/laravel-modules": "^8.0", "nwidart/laravel-modules": "^8.0",
"omnipay/paypal": "^3.0", "omnipay/paypal": "^3.0",
"payfast/payfast-php-sdk": "^1.1",
"pragmarx/google2fa": "^8.0", "pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"sentry/sentry-laravel": "^2", "sentry/sentry-laravel": "^2",
@ -86,6 +87,7 @@
"fakerphp/faker": "^1.14", "fakerphp/faker": "^1.14",
"filp/whoops": "^2.7", "filp/whoops": "^2.7",
"friendsofphp/php-cs-fixer": "^2.16", "friendsofphp/php-cs-fixer": "^2.16",
"laravel/dusk": "^6.15",
"mockery/mockery": "^1.3.1", "mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^5.0", "nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.0", "phpunit/phpunit": "^9.0",

242
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "16a38ffa3774d9d28a9f4c49366baac0", "content-hash": "d2beb37ff5fbee59ad4bb792e944eb10",
"packages": [ "packages": [
{ {
"name": "asm/php-ansible", "name": "asm/php-ansible",
@ -159,16 +159,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.185.7", "version": "3.185.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "7c0cd260e749374b5df247c4768c8f33f9a604e4" "reference": "667a83e4a18cb75db3ce74162efc97123da96261"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7c0cd260e749374b5df247c4768c8f33f9a604e4", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/667a83e4a18cb75db3ce74162efc97123da96261",
"reference": "7c0cd260e749374b5df247c4768c8f33f9a604e4", "reference": "667a83e4a18cb75db3ce74162efc97123da96261",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -243,9 +243,9 @@
"support": { "support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.185.7" "source": "https://github.com/aws/aws-sdk-php/tree/3.185.10"
}, },
"time": "2021-07-06T18:16:14+00:00" "time": "2021-07-09T19:21:22+00:00"
}, },
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -4113,16 +4113,16 @@
}, },
{ {
"name": "league/oauth1-client", "name": "league/oauth1-client",
"version": "v1.9.0", "version": "v1.9.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/oauth1-client.git", "url": "https://github.com/thephpleague/oauth1-client.git",
"reference": "1e7e6be2dc543bf466236fb171e5b20e1b06aee6" "reference": "19a3ce488bb1547c906209e8293199ec34eaa5b1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/1e7e6be2dc543bf466236fb171e5b20e1b06aee6", "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/19a3ce488bb1547c906209e8293199ec34eaa5b1",
"reference": "1e7e6be2dc543bf466236fb171e5b20e1b06aee6", "reference": "19a3ce488bb1547c906209e8293199ec34eaa5b1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4182,9 +4182,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/thephpleague/oauth1-client/issues", "issues": "https://github.com/thephpleague/oauth1-client/issues",
"source": "https://github.com/thephpleague/oauth1-client/tree/v1.9.0" "source": "https://github.com/thephpleague/oauth1-client/tree/v1.9.1"
}, },
"time": "2021-01-20T01:40:53+00:00" "time": "2021-07-07T22:54:46+00:00"
}, },
{ {
"name": "league/omnipay", "name": "league/omnipay",
@ -4251,16 +4251,16 @@
}, },
{ {
"name": "livewire/livewire", "name": "livewire/livewire",
"version": "v2.5.1", "version": "v2.5.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/livewire.git", "url": "https://github.com/livewire/livewire.git",
"reference": "a4ffb135693e7982e5b982ca203f5dc7a7ae1126" "reference": "1ca6757c78dbead4db7f52a72dabb8b27efcb3f6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/livewire/zipball/a4ffb135693e7982e5b982ca203f5dc7a7ae1126", "url": "https://api.github.com/repos/livewire/livewire/zipball/1ca6757c78dbead4db7f52a72dabb8b27efcb3f6",
"reference": "a4ffb135693e7982e5b982ca203f5dc7a7ae1126", "reference": "1ca6757c78dbead4db7f52a72dabb8b27efcb3f6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4311,7 +4311,7 @@
"description": "A front-end framework for Laravel.", "description": "A front-end framework for Laravel.",
"support": { "support": {
"issues": "https://github.com/livewire/livewire/issues", "issues": "https://github.com/livewire/livewire/issues",
"source": "https://github.com/livewire/livewire/tree/v2.5.1" "source": "https://github.com/livewire/livewire/tree/v2.5.3"
}, },
"funding": [ "funding": [
{ {
@ -4319,7 +4319,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2021-06-15T13:24:48+00:00" "time": "2021-07-08T13:58:45+00:00"
}, },
{ {
"name": "maennchen/zipstream-php", "name": "maennchen/zipstream-php",
@ -5334,6 +5334,57 @@
}, },
"time": "2020-10-15T08:29:30+00:00" "time": "2020-10-15T08:29:30+00:00"
}, },
{
"name": "payfast/payfast-php-sdk",
"version": "v1.1.2",
"source": {
"type": "git",
"url": "https://github.com/PayFast/payfast-php-sdk.git",
"reference": "1372980e38f381b84eed7eb46a40d5819a4fe58c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PayFast/payfast-php-sdk/zipball/1372980e38f381b84eed7eb46a40d5819a4fe58c",
"reference": "1372980e38f381b84eed7eb46a40d5819a4fe58c",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": ">=6.0.0",
"php": ">=7.2.5"
},
"require-dev": {
"phpunit/phpunit": "^9"
},
"type": "library",
"autoload": {
"psr-4": {
"PayFast\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Claire Grant",
"email": "claire.grant@payfast.co.za"
}
],
"description": "PayFast PHP Library",
"keywords": [
"api",
"onsite",
"payfast",
"php"
],
"support": {
"issues": "https://github.com/PayFast/payfast-php-sdk/issues",
"source": "https://github.com/PayFast/payfast-php-sdk/tree/v1.1.2"
},
"time": "2021-03-15T19:58:26+00:00"
},
{ {
"name": "php-http/client-common", "name": "php-http/client-common",
"version": "2.4.0", "version": "2.4.0",
@ -7319,16 +7370,16 @@
}, },
{ {
"name": "stripe/stripe-php", "name": "stripe/stripe-php",
"version": "v7.87.0", "version": "v7.88.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/stripe/stripe-php.git", "url": "https://github.com/stripe/stripe-php.git",
"reference": "9392f03cb8d8803bf8273378ce42d5cbbf1e24fc" "reference": "7203d00ba9b09830c0c5d5c06a9558db43b8e0ea"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/9392f03cb8d8803bf8273378ce42d5cbbf1e24fc", "url": "https://api.github.com/repos/stripe/stripe-php/zipball/7203d00ba9b09830c0c5d5c06a9558db43b8e0ea",
"reference": "9392f03cb8d8803bf8273378ce42d5cbbf1e24fc", "reference": "7203d00ba9b09830c0c5d5c06a9558db43b8e0ea",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7374,9 +7425,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/stripe/stripe-php/issues", "issues": "https://github.com/stripe/stripe-php/issues",
"source": "https://github.com/stripe/stripe-php/tree/v7.87.0" "source": "https://github.com/stripe/stripe-php/tree/v7.88.0"
}, },
"time": "2021-06-30T18:22:47+00:00" "time": "2021-07-09T20:01:03+00:00"
}, },
{ {
"name": "swiftmailer/swiftmailer", "name": "swiftmailer/swiftmailer",
@ -11899,6 +11950,79 @@
}, },
"time": "2020-07-09T08:09:16+00:00" "time": "2020-07-09T08:09:16+00:00"
}, },
{
"name": "laravel/dusk",
"version": "v6.15.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/dusk.git",
"reference": "45b55fa20321086c4f8cc4e712cbe54db644e21c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/dusk/zipball/45b55fa20321086c4f8cc4e712cbe54db644e21c",
"reference": "45b55fa20321086c4f8cc4e712cbe54db644e21c",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-zip": "*",
"illuminate/console": "^6.0|^7.0|^8.0",
"illuminate/support": "^6.0|^7.0|^8.0",
"nesbot/carbon": "^2.0",
"php": "^7.2|^8.0",
"php-webdriver/webdriver": "^1.9.0",
"symfony/console": "^4.3|^5.0",
"symfony/finder": "^4.3|^5.0",
"symfony/process": "^4.3|^5.0",
"vlucas/phpdotenv": "^3.0|^4.0|^5.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^4.16|^5.17.1|^6.12.1",
"phpunit/phpunit": "^7.5.15|^8.4|^9.0"
},
"suggest": {
"ext-pcntl": "Used to gracefully terminate Dusk when tests are running."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Dusk\\DuskServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Dusk\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Dusk provides simple end-to-end testing and browser automation.",
"keywords": [
"laravel",
"testing",
"webdriver"
],
"support": {
"issues": "https://github.com/laravel/dusk/issues",
"source": "https://github.com/laravel/dusk/tree/v6.15.0"
},
"time": "2021-04-06T14:14:57+00:00"
},
{ {
"name": "maximebf/debugbar", "name": "maximebf/debugbar",
"version": "v1.16.5", "version": "v1.16.5",
@ -12452,6 +12576,72 @@
}, },
"time": "2020-10-14T08:39:05+00:00" "time": "2020-10-14T08:39:05+00:00"
}, },
{
"name": "php-webdriver/webdriver",
"version": "1.11.1",
"source": {
"type": "git",
"url": "https://github.com/php-webdriver/php-webdriver.git",
"reference": "da16e39968f8dd5cfb7d07eef91dc2b731c69880"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/da16e39968f8dd5cfb7d07eef91dc2b731c69880",
"reference": "da16e39968f8dd5cfb7d07eef91dc2b731c69880",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-zip": "*",
"php": "^5.6 || ~7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.12",
"symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0"
},
"replace": {
"facebook/webdriver": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.0",
"ondram/ci-detector": "^2.1 || ^3.5 || ^4.0",
"php-coveralls/php-coveralls": "^2.4",
"php-mock/php-mock-phpunit": "^1.1 || ^2.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpunit/phpunit": "^5.7 || ^7 || ^8 || ^9",
"squizlabs/php_codesniffer": "^3.5",
"symfony/var-dumper": "^3.3 || ^4.0 || ^5.0"
},
"suggest": {
"ext-SimpleXML": "For Firefox profile creation"
},
"type": "library",
"autoload": {
"psr-4": {
"Facebook\\WebDriver\\": "lib/"
},
"files": [
"lib/Exception/TimeoutException.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
"homepage": "https://github.com/php-webdriver/php-webdriver",
"keywords": [
"Chromedriver",
"geckodriver",
"php",
"selenium",
"webdriver"
],
"support": {
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.11.1"
},
"time": "2021-05-21T15:12:49+00:00"
},
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
"version": "2.2.0", "version": "2.2.0",
@ -14691,5 +14881,5 @@
"platform-dev": { "platform-dev": {
"php": "^7.3|^7.4|^8.0" "php": "^7.3|^7.4|^8.0"
}, },
"plugin-api-version": "2.0.0" "plugin-api-version": "2.1.0"
} }

View File

@ -46,7 +46,7 @@ return [
'prefix' => '', 'prefix' => '',
'prefix_indexes' => true, 'prefix_indexes' => true,
'strict' => env('DB_STRICT', false), 'strict' => env('DB_STRICT', false),
'engine' => 'InnoDB ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8', // 'engine' => 'InnoDB ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8',
], ],
'sqlite' => [ 'sqlite' => [

View File

@ -109,7 +109,6 @@ return [
'gelf' => [ 'gelf' => [
'driver' => 'custom', 'driver' => 'custom',
'via' => \Hedii\LaravelGelfLogger\GelfLoggerFactory::class, 'via' => \Hedii\LaravelGelfLogger\GelfLoggerFactory::class,
// This optional option determines the processors that should be // This optional option determines the processors that should be

View File

@ -14,8 +14,8 @@ return [
'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', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.2.11', 'app_version' => '5.2.13',
'app_tag' => '5.2.11', 'app_tag' => '5.2.13',
'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', ''),
@ -82,6 +82,8 @@ return [
'checkout' => env('CHECKOUT_KEYS', ''), 'checkout' => env('CHECKOUT_KEYS', ''),
'travis' => env('TRAVIS', false), 'travis' => env('TRAVIS', false),
'test_email' => env('TEST_EMAIL', 'test@example.com'), 'test_email' => env('TEST_EMAIL', 'test@example.com'),
'wepay' => env('WEPAY_KEYS', ''),
'braintree' => env('BRAINTREE_KEYS', ''),
], ],
'contact' => [ 'contact' => [
'email' => env('MAIL_FROM_ADDRESS'), 'email' => env('MAIL_FROM_ADDRESS'),

View File

@ -1,10 +0,0 @@
{
"video": false,
"baseUrl": "http://localhost:8080/",
"chromeWebSecurity": false,
"env": {
"runningEnvironment": "docker"
},
"viewportWidth": 1280,
"viewportHeight": 800
}

View File

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

View File

@ -1,37 +0,0 @@
describe('Credits', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show credits page', () => {
cy.visit('/client/credits');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/credits');
});
});
it('should show credits text', () => {
cy.visit('/client/credits');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Credits');
});
/* it('should have required table elements', () => {
cy.visit('/client/credits');
cy.get('body')
.find('table.credits-table > tbody > tr')
.first()
.find('a')
.first()
.should('contain.text', 'View')
.click()
.location()
.should(location => {
expect(location.pathname).to.eq('/client/credits/VolejRejNm');
});
});*/
});

View File

@ -1,73 +0,0 @@
context('Invoices', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show invoices page', () => {
cy.visit('/client/invoices');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/invoices');
});
});
it('should show invoices text', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Invoices');
});
it('should show download and pay now buttons', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('button[value="download"]')
.first()
.should('contain.text', 'Download');
cy.get('body')
.find('button[value="payment"]')
.first()
.should('contain.text', 'Pay Now');
});
it('should have per page options dropdown', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
it('should have required table elements', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('table.invoices-table > tbody > tr')
.first()
.find('.button-link')
.first()
.should('contain.text', 'View')
.click()
.location()
.should(location => {
expect(location.pathname).to.eq('/client/invoices/VolejRejNm');
});
});
it('should filter table content', () => {
cy.visit('/client/invoices');
cy.get('body')
.find('#paid-checkbox')
.check();
cy.get('body')
.find('table.invoices-table > tbody > tr')
.first()
.should('not.contain', 'Overdue');
});
});

View File

@ -1,48 +0,0 @@
context('Login', () => {
beforeEach(() => {
cy.visit('/client/login');
});
it('should type into login form elements', () => {
cy.get('#test_email')
.invoke('val')
.then(emailValue => {
cy.get('#email')
.type(emailValue)
.should('have.value', emailValue);
});
cy.get('#test_password')
.invoke('val')
.then(passwordValue => {
cy.get('#password')
.type(passwordValue)
.should('have.value', passwordValue);
});
});
it('should login into client portal', () => {
cy.get('#test_email')
.invoke('val')
.then(emailValue => {
cy.get('#test_password')
.invoke('val')
.then(passwordValue => {
cy.get('#email')
.type(emailValue)
.should('have.value', emailValue);
cy.get('#password')
.type(passwordValue)
.should('have.value', passwordValue);
cy.get('#loginBtn')
.contains('Login')
.click();
cy.location().should(location => {
expect(location.pathname).to.eq(
'/client/invoices'
);
});
});
});
});
});

View File

@ -1,30 +0,0 @@
context('Payment methods', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show payment methods page', () => {
cy.visit('/client/payment_methods');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/payment_methods');
});
});
it('should show payment methods text', () => {
cy.visit('/client/payment_methods');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Payment Method');
});
it('should have per page options dropdown', () => {
cy.visit('/client/payment_methods');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
});

View File

@ -1,30 +0,0 @@
context('Payments', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show payments page', () => {
cy.visit('/client/payments');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/payments');
});
});
it('should show payments text', () => {
cy.visit('/client/payments');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Payments');
});
it('should have per page options dropdown', () => {
cy.visit('/client/payments');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
});

View File

@ -1,73 +0,0 @@
describe('Quotes', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show quotes page', () => {
cy.visit('/client/quotes');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/quotes');
});
});
it('should show quotes text', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Quotes');
});
it('should show download and approve buttons', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('button[value="download"]')
.first()
.should('contain.text', 'Download');
cy.get('body')
.find('button[value="approve"]')
.first()
.should('contain.text', 'Approve');
});
it('should have per page options dropdown', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
it('should have required table elements', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('table.quotes-table > tbody > tr')
.first()
.find('.button-link')
.first()
.should('contain.text', 'View')
.click()
.location()
.should(location => {
expect(location.pathname).to.eq('/client/quotes/VolejRejNm');
});
});
it('should filter table content', () => {
cy.visit('/client/quotes');
cy.get('body')
.find('#draft-checkbox')
.check();
cy.get('body')
.find('table.quotes-table > tbody > tr')
.first()
.should('not.contain', 'Sent');
});
});

View File

@ -1,31 +0,0 @@
context('Recurring invoices', () => {
beforeEach(() => {
cy.clientLogin();
});
it('should show recurring invoices page', () => {
cy.visit('/client/recurring_invoices');
cy.location().should(location => {
expect(location.pathname).to.eq('/client/recurring_invoices');
});
});
it('should show reucrring invoices text', () => {
cy.visit('/client/recurring_invoices');
cy.get('body')
.find('[data-ref=meta-title]')
.first()
.should('contain.text', 'Recurring Invoices');
});
it('should have per page options dropdown', () => {
cy.visit('/client/recurring_invoices');
cy.get('body')
.find('select')
.first()
.should('have.value', '10');
});
});

View File

@ -1,298 +0,0 @@
/// <reference types="cypress" />
context('Actions', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/actions')
})
// https://on.cypress.io/interacting-with-elements
it('.type() - type into a DOM element', () => {
// https://on.cypress.io/type
cy.get('.action-email')
.type('fake@email.com').should('have.value', 'fake@email.com')
// .type() with special character sequences
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
.type('{del}{selectall}{backspace}')
// .type() with key modifiers
.type('{alt}{option}') //these are equivalent
.type('{ctrl}{control}') //these are equivalent
.type('{meta}{command}{cmd}') //these are equivalent
.type('{shift}')
// Delay each keypress by 0.1 sec
.type('slow.typing@email.com', { delay: 100 })
.should('have.value', 'slow.typing@email.com')
cy.get('.action-disabled')
// Ignore error checking prior to type
// like whether the input is visible or disabled
.type('disabled error checking', { force: true })
.should('have.value', 'disabled error checking')
})
it('.focus() - focus on a DOM element', () => {
// https://on.cypress.io/focus
cy.get('.action-focus').focus()
.should('have.class', 'focus')
.prev().should('have.attr', 'style', 'color: orange;')
})
it('.blur() - blur off a DOM element', () => {
// https://on.cypress.io/blur
cy.get('.action-blur').type('About to blur').blur()
.should('have.class', 'error')
.prev().should('have.attr', 'style', 'color: red;')
})
it('.clear() - clears an input or textarea element', () => {
// https://on.cypress.io/clear
cy.get('.action-clear').type('Clear this text')
.should('have.value', 'Clear this text')
.clear()
.should('have.value', '')
})
it('.submit() - submit a form', () => {
// https://on.cypress.io/submit
cy.get('.action-form')
.find('[type="text"]').type('HALFOFF')
cy.get('.action-form').submit()
.next().should('contain', 'Your form has been submitted!')
})
it('.click() - click on a DOM element', () => {
// https://on.cypress.io/click
cy.get('.action-btn').click()
// You can click on 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// clicking in the center of the element is the default
cy.get('#action-canvas').click()
cy.get('#action-canvas').click('topLeft')
cy.get('#action-canvas').click('top')
cy.get('#action-canvas').click('topRight')
cy.get('#action-canvas').click('left')
cy.get('#action-canvas').click('right')
cy.get('#action-canvas').click('bottomLeft')
cy.get('#action-canvas').click('bottom')
cy.get('#action-canvas').click('bottomRight')
// .click() accepts an x and y coordinate
// that controls where the click occurs :)
cy.get('#action-canvas')
.click(80, 75) // click 80px on x coord and 75px on y coord
.click(170, 75)
.click(80, 165)
.click(100, 185)
.click(125, 190)
.click(150, 185)
.click(170, 165)
// click multiple elements by passing multiple: true
cy.get('.action-labels>.label').click({ multiple: true })
// Ignore error checking prior to clicking
cy.get('.action-opacity>.btn').click({ force: true })
})
it('.dblclick() - double click on a DOM element', () => {
// https://on.cypress.io/dblclick
// Our app has a listener on 'dblclick' event in our 'scripts.js'
// that hides the div and shows an input on double click
cy.get('.action-div').dblclick().should('not.be.visible')
cy.get('.action-input-hidden').should('be.visible')
})
it('.rightclick() - right click on a DOM element', () => {
// https://on.cypress.io/rightclick
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
// that hides the div and shows an input on right click
cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
cy.get('.rightclick-action-input-hidden').should('be.visible')
})
it('.check() - check a checkbox or radio element', () => {
// https://on.cypress.io/check
// By default, .check() will check all
// matching checkbox or radio elements in succession, one after another
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
.check().should('be.checked')
cy.get('.action-radios [type="radio"]').not('[disabled]')
.check().should('be.checked')
// .check() accepts a value argument
cy.get('.action-radios [type="radio"]')
.check('radio1').should('be.checked')
// .check() accepts an array of values
cy.get('.action-multiple-checkboxes [type="checkbox"]')
.check(['checkbox1', 'checkbox2']).should('be.checked')
// Ignore error checking prior to checking
cy.get('.action-checkboxes [disabled]')
.check({ force: true }).should('be.checked')
cy.get('.action-radios [type="radio"]')
.check('radio3', { force: true }).should('be.checked')
})
it('.uncheck() - uncheck a checkbox element', () => {
// https://on.cypress.io/uncheck
// By default, .uncheck() will uncheck all matching
// checkbox elements in succession, one after another
cy.get('.action-check [type="checkbox"]')
.not('[disabled]')
.uncheck().should('not.be.checked')
// .uncheck() accepts a value argument
cy.get('.action-check [type="checkbox"]')
.check('checkbox1')
.uncheck('checkbox1').should('not.be.checked')
// .uncheck() accepts an array of values
cy.get('.action-check [type="checkbox"]')
.check(['checkbox1', 'checkbox3'])
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
// Ignore error checking prior to unchecking
cy.get('.action-check [disabled]')
.uncheck({ force: true }).should('not.be.checked')
})
it('.select() - select an option in a <select> element', () => {
// https://on.cypress.io/select
// at first, no option should be selected
cy.get('.action-select')
.should('have.value', '--Select a fruit--')
// Select option(s) with matching text content
cy.get('.action-select').select('apples')
// confirm the apples were selected
// note that each value starts with "fr-" in our HTML
cy.get('.action-select').should('have.value', 'fr-apples')
cy.get('.action-select-multiple')
.select(['apples', 'oranges', 'bananas'])
// when getting multiple values, invoke "val" method first
.invoke('val')
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
// Select option(s) with matching value
cy.get('.action-select').select('fr-bananas')
// can attach an assertion right away to the element
.should('have.value', 'fr-bananas')
cy.get('.action-select-multiple')
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
.invoke('val')
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
// assert the selected values include oranges
cy.get('.action-select-multiple')
.invoke('val').should('include', 'fr-oranges')
})
it('.scrollIntoView() - scroll an element into view', () => {
// https://on.cypress.io/scrollintoview
// normally all of these buttons are hidden,
// because they're not within
// the viewable area of their parent
// (we need to scroll to see them)
cy.get('#scroll-horizontal button')
.should('not.be.visible')
// scroll the button into view, as if the user had scrolled
cy.get('#scroll-horizontal button').scrollIntoView()
.should('be.visible')
cy.get('#scroll-vertical button')
.should('not.be.visible')
// Cypress handles the scroll direction needed
cy.get('#scroll-vertical button').scrollIntoView()
.should('be.visible')
cy.get('#scroll-both button')
.should('not.be.visible')
// Cypress knows to scroll to the right and down
cy.get('#scroll-both button').scrollIntoView()
.should('be.visible')
})
it('.trigger() - trigger an event on a DOM element', () => {
// https://on.cypress.io/trigger
// To interact with a range input (slider)
// we need to set its value & trigger the
// event to signal it changed
// Here, we invoke jQuery's val() method to set
// the value and trigger the 'change' event
cy.get('.trigger-input-range')
.invoke('val', 25)
.trigger('change')
.get('input[type=range]').siblings('p')
.should('have.text', '25')
})
it('cy.scrollTo() - scroll the window or element to a position', () => {
// https://on.cypress.io/scrollTo
// You can scroll to 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// if you chain .scrollTo() off of cy, we will
// scroll the entire window
cy.scrollTo('bottom')
cy.get('#scrollable-horizontal').scrollTo('right')
// or you can scroll to a specific coordinate:
// (x axis, y axis) in pixels
cy.get('#scrollable-vertical').scrollTo(250, 250)
// or you can scroll to a specific percentage
// of the (width, height) of the element
cy.get('#scrollable-both').scrollTo('75%', '25%')
// control the easing of the scroll (default is 'swing')
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
// control the duration of the scroll (in ms)
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
})
})

View File

@ -1,42 +0,0 @@
/// <reference types="cypress" />
context('Aliasing', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/aliasing')
})
it('.as() - alias a DOM element for later use', () => {
// https://on.cypress.io/as
// Alias a DOM element for use later
// We don't have to traverse to the element
// later in our code, we reference it with @
cy.get('.as-table').find('tbody>tr')
.first().find('td').first()
.find('button').as('firstBtn')
// when we reference the alias, we place an
// @ in front of its name
cy.get('@firstBtn').click()
cy.get('@firstBtn')
.should('have.class', 'btn-success')
.and('contain', 'Changed')
})
it('.as() - alias a route for later use', () => {
// Alias the route to wait for its response
cy.server()
cy.route('GET', 'comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// https://on.cypress.io/wait
cy.wait('@getComment').its('status').should('eq', 200)
})
})

View File

@ -1,168 +0,0 @@
/// <reference types="cypress" />
context('Assertions', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/assertions')
})
describe('Implicit Assertions', () => {
it('.should() - make an assertion about the current subject', () => {
// https://on.cypress.io/should
cy.get('.assertion-table')
.find('tbody tr:last')
.should('have.class', 'success')
.find('td')
.first()
// checking the text of the <td> element in various ways
.should('have.text', 'Column content')
.should('contain', 'Column content')
.should('have.html', 'Column content')
// chai-jquery uses "is()" to check if element matches selector
.should('match', 'td')
// to match text content against a regular expression
// first need to invoke jQuery method text()
// and then match using regular expression
.invoke('text')
.should('match', /column content/i)
// a better way to check element's text content against a regular expression
// is to use "cy.contains"
// https://on.cypress.io/contains
cy.get('.assertion-table')
.find('tbody tr:last')
// finds first <td> element with text content matching regular expression
.contains('td', /column content/i)
.should('be.visible')
// for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents
})
it('.and() - chain multiple assertions together', () => {
// https://on.cypress.io/and
cy.get('.assertions-link')
.should('have.class', 'active')
.and('have.attr', 'href')
.and('include', 'cypress.io')
})
})
describe('Explicit Assertions', () => {
// https://on.cypress.io/assertions
it('expect - make an assertion about a specified subject', () => {
// We can use Chai's BDD style assertions
expect(true).to.be.true
const o = { foo: 'bar' }
expect(o).to.equal(o)
expect(o).to.deep.equal({ foo: 'bar' })
// matching text using regular expression
expect('FooBar').to.match(/bar$/i)
})
it('pass your own callback function to should()', () => {
// Pass a function to should that can have any number
// of explicit assertions within it.
// The ".should(cb)" function will be retried
// automatically until it passes all your explicit assertions or times out.
cy.get('.assertions-p')
.find('p')
.should(($p) => {
// https://on.cypress.io/$
// return an array of texts from all of the p's
// @ts-ignore TS6133 unused variable
const texts = $p.map((i, el) => Cypress.$(el).text())
// jquery map returns jquery object
// and .get() convert this to simple array
const paragraphs = texts.get()
// array should have length of 3
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
// use second argument to expect(...) to provide clear
// message with each assertion
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
'Some text from first p',
'More text from second p',
'And even more text from third p',
])
})
})
it('finds element by class name regex', () => {
cy.get('.docs-header')
.find('div')
// .should(cb) callback function will be retried
.should(($div) => {
expect($div).to.have.length(1)
const className = $div[0].className
expect(className).to.match(/heading-/)
})
// .then(cb) callback is not retried,
// it either passes or fails
.then(($div) => {
expect($div, 'text content').to.have.text('Introduction')
})
})
it('can throw any error', () => {
cy.get('.docs-header')
.find('div')
.should(($div) => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error('Did not find 1 element')
}
const className = $div[0].className
if (!className.match(/heading-/)) {
throw new Error(`Could not find class "heading-" in ${className}`)
}
})
})
it('matches unknown text between two elements', () => {
/**
* Text from the first element.
* @type {string}
*/
let text
/**
* Normalizes passed text,
* useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize
*/
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
cy.get('.two-elements')
.find('.first')
.then(($first) => {
// save text from the first element
text = normalizeText($first.text())
})
cy.get('.two-elements')
.find('.second')
.should(($div) => {
// we can massage text before comparing
const secondText = normalizeText($div.text())
expect(secondText, 'second text').to.equal(text)
})
})
it('assert - assert shape of an object', () => {
const person = {
name: 'Joe',
age: 20,
}
assert.isObject(person, 'value is object')
})
})
})

View File

@ -1,97 +0,0 @@
/// <reference types="cypress" />
context('Connectors', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/connectors')
})
it('.each() - iterate over an array of elements', () => {
// https://on.cypress.io/each
cy.get('.connectors-each-ul>li')
.each(($el, index, $list) => {
console.log($el, index, $list)
})
})
it('.its() - get properties on the current subject', () => {
// https://on.cypress.io/its
cy.get('.connectors-its-ul>li')
// calls the 'length' property yielding that value
.its('length')
.should('be.gt', 2)
})
it('.invoke() - invoke a function on the current subject', () => {
// our div is hidden in our script.js
// $('.connectors-div').hide()
// https://on.cypress.io/invoke
cy.get('.connectors-div').should('be.hidden')
// call the jquery method 'show' on the 'div.container'
.invoke('show')
.should('be.visible')
})
it('.spread() - spread an array as individual args to callback function', () => {
// https://on.cypress.io/spread
const arr = ['foo', 'bar', 'baz']
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq('foo')
expect(bar).to.eq('bar')
expect(baz).to.eq('baz')
})
})
describe('.then()', () => {
it('invokes a callback function with the current subject', () => {
// https://on.cypress.io/then
cy.get('.connectors-list > li')
.then(($lis) => {
expect($lis, '3 items').to.have.length(3)
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
})
})
it('yields the returned value to the next command', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
return 2
})
.then((num) => {
expect(num).to.equal(2)
})
})
it('yields the original subject without return', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
// note that nothing is returned from this callback
})
.then((num) => {
// this callback receives the original unchanged value 1
expect(num).to.equal(1)
})
})
it('yields the value yielded by the last Cypress command inside', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
// note how we run a Cypress command
// the result yielded by this Cypress command
// will be passed to the second ".then"
cy.wrap(2)
})
.then((num) => {
// this callback receives the value yielded by "cy.wrap(2)"
expect(num).to.equal(2)
})
})
})
})

View File

@ -1,78 +0,0 @@
/// <reference types="cypress" />
context('Cookies', () => {
beforeEach(() => {
Cypress.Cookies.debug(true)
cy.visit('https://example.cypress.io/commands/cookies')
// clear cookies again after visiting to remove
// any 3rd party cookies picked up such as cloudflare
cy.clearCookies()
})
it('cy.getCookie() - get a browser cookie', () => {
// https://on.cypress.io/getcookie
cy.get('#getCookie .set-a-cookie').click()
// cy.getCookie() yields a cookie object
cy.getCookie('token').should('have.property', 'value', '123ABC')
})
it('cy.getCookies() - get browser cookies', () => {
// https://on.cypress.io/getcookies
cy.getCookies().should('be.empty')
cy.get('#getCookies .set-a-cookie').click()
// cy.getCookies() yields an array of cookies
cy.getCookies().should('have.length', 1).should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property('name', 'token')
expect(cookies[0]).to.have.property('value', '123ABC')
expect(cookies[0]).to.have.property('httpOnly', false)
expect(cookies[0]).to.have.property('secure', false)
expect(cookies[0]).to.have.property('domain')
expect(cookies[0]).to.have.property('path')
})
})
it('cy.setCookie() - set a browser cookie', () => {
// https://on.cypress.io/setcookie
cy.getCookies().should('be.empty')
cy.setCookie('foo', 'bar')
// cy.getCookie() yields a cookie object
cy.getCookie('foo').should('have.property', 'value', 'bar')
})
it('cy.clearCookie() - clear a browser cookie', () => {
// https://on.cypress.io/clearcookie
cy.getCookie('token').should('be.null')
cy.get('#clearCookie .set-a-cookie').click()
cy.getCookie('token').should('have.property', 'value', '123ABC')
// cy.clearCookies() yields null
cy.clearCookie('token').should('be.null')
cy.getCookie('token').should('be.null')
})
it('cy.clearCookies() - clear browser cookies', () => {
// https://on.cypress.io/clearcookies
cy.getCookies().should('be.empty')
cy.get('#clearCookies .set-a-cookie').click()
cy.getCookies().should('have.length', 1)
// cy.clearCookies() yields null
cy.clearCookies()
cy.getCookies().should('be.empty')
})
})

View File

@ -1,222 +0,0 @@
/// <reference types="cypress" />
context('Cypress.Commands', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/custom-commands
it('.add() - create a custom command', () => {
Cypress.Commands.add('console', {
prevSubject: true,
}, (subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used
method = method || 'log'
// log the subject to the console
// @ts-ignore TS7017
console[method]('The subject is', subject)
// whatever we return becomes the new subject
// we don't want to change the subject so
// we return whatever was passed in
return subject
})
// @ts-ignore TS2339
cy.get('button').console('info').then(($button) => {
// subject is still $button
})
})
})
context('Cypress.Cookies', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/cookies
it('.debug() - enable or disable debugging', () => {
Cypress.Cookies.debug(true)
// Cypress will now log in the console when
// cookies are set or cleared
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
})
it('.preserveOnce() - preserve cookies by key', () => {
// normally cookies are reset after each test
cy.getCookie('fakeCookie').should('not.be.ok')
// preserving a cookie will not clear it when
// the next test starts
cy.setCookie('lastCookie', '789XYZ')
Cypress.Cookies.preserveOnce('lastCookie')
})
it('.defaults() - set defaults for all cookies', () => {
// now any cookie with the name 'session_id' will
// not be cleared before each new test runs
Cypress.Cookies.defaults({
whitelist: 'session_id',
})
})
})
context('Cypress.Server', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// Permanently override server options for
// all instances of cy.server()
// https://on.cypress.io/cypress-server
it('.defaults() - change default config of server', () => {
Cypress.Server.defaults({
delay: 0,
force404: false,
})
})
})
context('Cypress.arch', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get CPU architecture name of underlying OS', () => {
// https://on.cypress.io/arch
expect(Cypress.arch).to.exist
})
})
context('Cypress.config()', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get and set configuration options', () => {
// https://on.cypress.io/config
let myConfig = Cypress.config()
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
expect(myConfig).to.have.property('baseUrl', null)
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
expect(myConfig).to.have.property('requestTimeout', 5000)
expect(myConfig).to.have.property('responseTimeout', 30000)
expect(myConfig).to.have.property('viewportHeight', 660)
expect(myConfig).to.have.property('viewportWidth', 1000)
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
expect(myConfig).to.have.property('waitForAnimations', true)
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
// this will change the config for the rest of your tests!
Cypress.config('pageLoadTimeout', 20000)
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
Cypress.config('pageLoadTimeout', 60000)
})
})
context('Cypress.dom', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/dom
it('.isHidden() - determine if a DOM element is hidden', () => {
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
let visibleP = Cypress.$('.dom-p p.visible').get(0)
// our first paragraph has css class 'hidden'
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
expect(Cypress.dom.isHidden(visibleP)).to.be.false
})
})
context('Cypress.env()', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// We can set environment variables for highly dynamic values
// https://on.cypress.io/environment-variables
it('Get environment variables', () => {
// https://on.cypress.io/env
// set multiple environment variables
Cypress.env({
host: 'veronica.dev.local',
api_server: 'http://localhost:8888/v1/',
})
// get environment variable
expect(Cypress.env('host')).to.eq('veronica.dev.local')
// set environment variable
Cypress.env('api_server', 'http://localhost:8888/v2/')
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
// get all environment variable
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
})
})
context('Cypress.log', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Control what is printed to the Command Log', () => {
// https://on.cypress.io/cypress-log
})
})
context('Cypress.platform', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get underlying OS name', () => {
// https://on.cypress.io/platform
expect(Cypress.platform).to.be.exist
})
})
context('Cypress.version', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get current version of Cypress being run', () => {
// https://on.cypress.io/version
expect(Cypress.version).to.be.exist
})
})
context('Cypress.spec', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get current spec information', () => {
// https://on.cypress.io/spec
// wrap the object so we can inspect it easily by clicking in the command log
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
})
})

View File

@ -1,114 +0,0 @@
/// <reference types="cypress" />
/// JSON fixture file can be loaded directly using
// the built-in JavaScript bundler
// @ts-ignore
const requiredExample = require('../../fixtures/example')
context('Files', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/files')
})
beforeEach(() => {
// load example.json fixture file and store
// in the test context object
cy.fixture('example.json').as('example')
})
it('cy.fixture() - load a fixture', () => {
// https://on.cypress.io/fixture
// Instead of writing a response inline you can
// use a fixture file's content.
cy.server()
cy.fixture('example.json').as('comment')
// when application makes an Ajax request matching "GET comments/*"
// Cypress will intercept it and reply with object
// from the "comment" alias
cy.route('GET', 'comments/*', '@comment').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
// you can also just write the fixture in the route
cy.route('GET', 'comments/*', 'fixture:example.json').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
// or write fx to represent fixture
// by default it assumes it's .json
cy.route('GET', 'comments/*', 'fx:example').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('responseBody')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
})
it('cy.fixture() or require - load a fixture', function () {
// we are inside the "function () { ... }"
// callback and can use test context object "this"
// "this.example" was loaded in "beforeEach" function callback
expect(this.example, 'fixture in the test context')
.to.deep.equal(requiredExample)
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
// @ts-ignore
cy.wrap(this.example, 'fixture vs require')
.should('deep.equal', requiredExample)
})
it('cy.readFile() - read file contents', () => {
// https://on.cypress.io/readfile
// You can read a file and yield its contents
// The filePath is relative to your project's root.
cy.readFile('cypress.json').then((json) => {
expect(json).to.be.an('object')
})
})
it('cy.writeFile() - write to a file', () => {
// https://on.cypress.io/writefile
// You can write to a file
// Use a response from a request to automatically
// generate a fixture file for use later
cy.request('https://jsonplaceholder.cypress.io/users')
.then((response) => {
cy.writeFile('cypress/fixtures/users.json', response.body)
})
cy.fixture('users').should((users) => {
expect(users[0].name).to.exist
})
// JavaScript arrays and objects are stringified
// and formatted into text.
cy.writeFile('cypress/fixtures/profile.json', {
id: 8739,
name: 'Jane',
email: 'jane@example.com',
})
cy.fixture('profile').should((profile) => {
expect(profile.name).to.eq('Jane')
})
})
})

View File

@ -1,52 +0,0 @@
/// <reference types="cypress" />
context('Local Storage', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/local-storage')
})
// Although local storage is automatically cleared
// in between tests to maintain a clean state
// sometimes we need to clear the local storage manually
it('cy.clearLocalStorage() - clear all data in local storage', () => {
// https://on.cypress.io/clearlocalstorage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
// clearLocalStorage() yields the localStorage object
cy.clearLocalStorage().should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.be.null
expect(ls.getItem('prop3')).to.be.null
})
// Clear key matching string in Local Storage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
cy.clearLocalStorage('prop1').should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.eq('blue')
expect(ls.getItem('prop3')).to.eq('magenta')
})
// Clear keys matching regex in Local Storage
cy.get('.ls-btn').click().should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
cy.clearLocalStorage(/prop1|2/).should((ls) => {
expect(ls.getItem('prop1')).to.be.null
expect(ls.getItem('prop2')).to.be.null
expect(ls.getItem('prop3')).to.eq('magenta')
})
})
})

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