1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-12 06:02:39 +01:00

Merge pull request #6910 from turbo124/v5-stable

V5 stable
This commit is contained in:
David Bomba 2021-10-27 14:32:40 +11:00 committed by GitHub
commit c450ccc4bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 470 additions and 322 deletions

View File

@ -1 +1 @@
5.3.27
5.3.29

View File

@ -52,6 +52,8 @@ class Handler extends ExceptionHandler
MaxAttemptsExceededException::class,
CommandNotFoundException::class,
ValidationException::class,
ModelNotFoundException::class,
NotFoundHttpException::class,
];
/**

View File

@ -15,6 +15,7 @@ use App\Http\Requests\Activity\DownloadHistoricalEntityRequest;
use App\Models\Activity;
use App\Transformers\ActivityTransformer;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\Ninja;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\Pdf\PdfMaker;
use Illuminate\Http\JsonResponse;
@ -147,7 +148,12 @@ class ActivityController extends BaseController
*/
if($backup && $backup->filename && Storage::disk(config('filesystems.default'))->exists($backup->filename)){ //disk
$html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->path($backup->filename));
if(Ninja::isHosted())
$html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->url($backup->filename));
else
$html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->path($backup->filename));
}
elseif($backup && $backup->html_backup){ //db
$html_backup = $backup->html_backup;

View File

@ -41,7 +41,16 @@ class ContactLoginController extends Controller
// $company = null;
// }else
if (strpos($request->getHost(), 'invoicing.co') !== false) {
$company = false;
if($request->has('company_key')){
MultiDB::findAndSetDbByCompanyKey($request->input('company_key'));
$company = Company::where('company_key', $request->input('company_key'))->first();
}
if (!$company && strpos($request->getHost(), 'invoicing.co') !== false) {
$subdomain = explode('.', $request->getHost())[0];
MultiDB::findAndSetDbByDomain(['subdomain' => $subdomain]);
@ -72,8 +81,8 @@ class ContactLoginController extends Controller
{
Auth::shouldUse('contact');
if(Ninja::isHosted() && $request->has('db'))
MultiDB::setDb($request->input('db'));
if(Ninja::isHosted() && $request->has('company_key'))
MultiDB::findAndSetDbByCompanyKey($request->input('company_key'));
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle

View File

@ -27,7 +27,7 @@ class SwitchCompanyController extends Controller
->where('id', $this->transformKeys($contact))
->first();
auth()->guard('contact')->user()->login($client_contact, true);
auth()->guard('contact')->login($client_contact, true);
return redirect('/client/dashboard');
}

View File

@ -240,7 +240,7 @@ class CompanyController extends BaseController
/*
* Create token
*/
$user_agent = request()->input('token_name') ?: request()->server('HTTP_USER_AGENT');
$user_agent = request()->has('token_name') ? request()->input('token_name') : request()->server('HTTP_USER_AGENT');
$company_token = CreateCompanyToken::dispatchNow($company, auth()->user(), $user_agent);

View File

@ -683,8 +683,6 @@ class PaymentController extends BaseController
{
$payment = $request->payment();
// nlog($request->all());
$payment = $payment->refund($request->all());
return $this->itemResponse($payment);

View File

@ -42,6 +42,9 @@ class StoreRecurringExpenseRequest extends Request
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id;
$rules['frequency_id'] = 'required|integer|digits_between:1,12';
$rules['tax_amount1'] = 'numeric';
$rules['tax_amount2'] = 'numeric';
$rules['tax_amount3'] = 'numeric';
return $this->globalRules($rules);
}

View File

@ -43,6 +43,10 @@ class UpdateRecurringExpenseRequest extends Request
$rules['number'] = Rule::unique('recurring_expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_expense->id);
}
$rules['tax_amount1'] = 'numeric';
$rules['tax_amount2'] = 'numeric';
$rules['tax_amount3'] = 'numeric';
return $this->globalRules($rules);
}

View File

@ -33,9 +33,10 @@ class UpdateTaskStatusRequest extends Request
{
$rules = [];
if ($this->input('name')) {
$rules['name'] = Rule::unique('task_statuses')->where('company_id', auth()->user()->company()->id)->ignore($this->task_status->id);
}
// 26/10/2021 we disable this as it prevent updating existing task status meta data where the same name already exists
// if ($this->input('name')) {
// $rules['name'] = Rule::unique('task_statuses')->where('company_id', auth()->user()->company()->id)->ignore($this->task_status->id);
// }
return $rules;

View File

@ -51,7 +51,7 @@ class PaymentAppliedValidAmount implements Rule
$payment_amounts = 0;
$invoice_amounts = 0;
$payment_amounts = $payment->amount - $payment->applied;
$payment_amounts = $payment->amount - $payment->refunded - $payment->applied;
if (request()->input('credits') && is_array(request()->input('credits'))) {
foreach (request()->input('credits') as $credit) {

View File

@ -26,7 +26,10 @@ class ValidAmount implements Rule
*/
public function passes($attribute, $value)
{
return trim($value, '-1234567890.,') === '';
return is_numeric((string)$value);
//return filter_var((string)$value, FILTER_VALIDATE_FLOAT);
// return preg_match('^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$^', (string)$value);
// return trim($value, '-1234567890.,') === '';
}

View File

@ -71,7 +71,7 @@ class RecurringInvoicesCron
SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db);
}
catch(\Exception $e){
nlog("Unable to sending recurring invoice {$recurring_invoice->id}");
nlog("Unable to sending recurring invoice {$recurring_invoice->id} ". $e->getMessage());
}
});
@ -107,7 +107,7 @@ class RecurringInvoicesCron
SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db);
}
catch(\Exception $e){
nlog("Unable to sending recurring invoice {$recurring_invoice->id}");
nlog("Unable to sending recurring invoice {$recurring_invoice->id} ". $e->getMessage());
}
});

View File

@ -15,6 +15,7 @@ use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Admin\ClientPaymentFailureObject;
use App\Mail\Admin\EntityNotificationMailer;
use App\Mail\Admin\PaymentFailureObject;
use App\Models\Client;
@ -102,6 +103,24 @@ class PaymentFailedMailer implements ShouldQueue
});
//add client payment failures here.
nlog("pre client failure email");
if($contact = $this->client->primary_contact()->first())
{
nlog("inside failure");
$mail_obj = (new ClientPaymentFailureObject($this->client, $this->error, $this->company, $this->payment_hash))->build();
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer($mail_obj);
$nmo->company = $this->company;
$nmo->to_user = $contact;
$nmo->settings = $settings;
NinjaMailerJob::dispatch($nmo);
}
}

View File

@ -73,13 +73,14 @@ class SendRecurring implements ShouldQueue
$invoice->date = now()->format('Y-m-d');
$invoice->due_date = $this->recurring_invoice->calculateDueDate(now()->format('Y-m-d'));
$invoice->recurring_id = $this->recurring_invoice->id;
$invoice->saveQuietly();
if($invoice->client->getSetting('auto_email_invoice'))
{
$invoice = $invoice->service()
->markSent()
->applyNumber()
// ->createInvitations() //need to only link invitations to those in the recurring invoice
//->createInvitations() //need to only link invitations to those in the recurring invoice
->fillDefaults()
->save();

View File

@ -14,7 +14,6 @@ namespace App\Mail\Admin;
use App\Models\Invoice;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\App;
use stdClass;
@ -91,7 +90,7 @@ class ClientPaymentFailureObject
return
ctrans(
'texts.notification_invoice_payment_failed_subject',
['invoice' => $this->client->present()->name()]
['invoice' => implode(",", $this->invoices->pluck('number')->toArray())]
);
}
@ -110,7 +109,7 @@ class ClientPaymentFailureObject
]
),
'greeting' => ctrans('texts.email_salutation', ['name' => $this->client->present()->name]),
'message' => $this->error,
'message' => ctrans('texts.client_payment_failure_body', ['invoice' => implode(",", $this->invoices->pluck('number')->toArray()), 'amount' => $this->getAmount()]),
'signature' => $signature,
'logo' => $this->company->present()->logo(),
'settings' => $this->client->getMergedSettings(),

View File

@ -92,7 +92,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
'custom_value4',
'email',
'is_primary',
// 'client_id',
'send_email',
];
/**

View File

@ -145,7 +145,9 @@ class Gateway extends StaticModel
GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
];
break;
case 57:

View File

@ -84,7 +84,7 @@ class GatewayType extends StaticModel
case self::EPS:
return ctrans('texts.eps');
case self::BECS:
return ctrans('tets.becs');
return ctrans('texts.becs');
case self::ACSS:
return ctrans('texts.acss');
case self::DIRECT_DEBIT:

View File

@ -221,6 +221,19 @@ class BaseDriver extends AbstractPaymentDriver
{
$this->confirmGatewayFee();
/*Never create a payment with a duplicate transaction reference*/
if(array_key_exists('transaction_reference', $data)){
$_payment = Payment::where('transaction_reference', $data['transaction_reference'])
->where('client_id', $this->client->id)
->first();
if($_payment)
return $_payment;
}
$payment = PaymentFactory::create($this->client->company->id, $this->client->user->id);
$payment->client_id = $this->client->id;
$payment->company_gateway_id = $this->company_gateway->id;

View File

@ -13,7 +13,6 @@ namespace App\PaymentDrivers\Braintree;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
@ -23,6 +22,7 @@ use App\Models\SystemLog;
use App\PaymentDrivers\BraintreePaymentDriver;
use App\PaymentDrivers\Common\MethodInterface;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
class ACH implements MethodInterface
{

View File

@ -12,8 +12,6 @@
namespace App\PaymentDrivers;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
@ -27,7 +25,6 @@ use App\PaymentDrivers\Braintree\CreditCard;
use App\PaymentDrivers\Braintree\PayPal;
use Braintree\Gateway;
use Exception;
use Illuminate\Http\Request;
class BraintreePaymentDriver extends BaseDriver
{
@ -40,7 +37,7 @@ class BraintreePaymentDriver extends BaseDriver
/**
* @var Gateway;
*/
public $gateway;
public Gateway $gateway;
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
@ -118,8 +115,7 @@ class BraintreePaymentDriver extends BaseDriver
]);
if ($result->success) {
$address = $this->gateway->address()->create([
$address = $this->gateway->address()->create([
'customerId' => $result->customer->id,
'firstName' => $this->client->present()->name,
'streetAddress' => $this->client->address1,
@ -135,12 +131,9 @@ class BraintreePaymentDriver extends BaseDriver
{
$this->init();
try{
try {
$response = $this->gateway->transaction()->refund($payment->transaction_reference, $amount);
} catch (Exception $e) {
$data = [
'transaction_reference' => null,
'transaction_response' => json_encode($e->getMessage()),
@ -154,24 +147,19 @@ class BraintreePaymentDriver extends BaseDriver
return $data;
}
if($response->success)
{
if ($response->success) {
$data = [
'transaction_reference' => $response->id,
'transaction_reference' => $payment->transaction_reference,
'transaction_response' => json_encode($response),
'success' => (bool)$response->success,
'description' => $response->status,
'success' => (bool) $response->success,
'description' => ctrans('texts.plan_refunded'),
'code' => 0,
];
SystemLogger::dispatch(['server_response' => $response, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_BRAINTREE, $this->client, $this->client->company);
return $data;
}
else{
} else {
$error = $response->errors->deepAll()[0];
$data = [
@ -185,7 +173,6 @@ class BraintreePaymentDriver extends BaseDriver
SystemLogger::dispatch(['server_response' => $response, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_BRAINTREE, $this->client, $this->client->company);
return $data;
}
}

View File

@ -14,7 +14,7 @@ namespace App\PaymentDrivers\CheckoutCom;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\PaymentDrivers\CheckoutComPaymentDriver;
@ -112,7 +112,7 @@ class CreditCard implements MethodInterface
$data['currency'] = $this->checkout->client->getCurrencyCode();
$data['value'] = $this->checkout->convertToCheckoutAmount($data['total']['amount_with_fee'], $this->checkout->client->getCurrencyCode());
$data['raw_value'] = $data['total']['amount_with_fee'];
$data['customer_email'] = $this->checkout->client->present()->email;
$data['customer_email'] = $this->checkout->client->present()->email();
return render('gateways.checkout.credit_card.pay', $data);
}
@ -173,6 +173,10 @@ class CreditCard implements MethodInterface
$payment = new Payment($method, $this->checkout->payment_hash->data->currency);
$payment->amount = $this->checkout->payment_hash->data->value;
$payment->reference = $this->checkout->getDescription();
$payment->customer = [
'name' => $this->checkout->client->present()->name() ,
'email' => $this->checkout->client->present()->email(),
];
$this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, ['checkout_payment_ref' => $payment]);
$this->checkout->payment_hash->save();

View File

@ -84,8 +84,7 @@ trait Utilities
public function processUnsuccessfulPayment(Payment $_payment, $throw_exception = true)
{
$this->getParent()->sendFailureMail($_payment->status . " " . $_payment->response_summary);
$this->getParent()->sendFailureMail($_payment->status . " " . optional($_payment)->response_summary);
$message = [
'server_response' => $_payment,
@ -102,7 +101,7 @@ trait Utilities
);
if ($throw_exception) {
throw new PaymentFailed($_payment->status . " " . $_payment->response_summary, $_payment->http_code);
throw new PaymentFailed($_payment->status . " " . optional($_payment)->response_summary, $_payment->http_code);
}
}

View File

@ -338,7 +338,9 @@ class CheckoutComPaymentDriver extends BaseDriver
$this->setPaymentHash($request->getPaymentHash());
try {
$payment = $this->gateway->payments()->details($request->query('cko-session-id'));
$payment = $this->gateway->payments()->details(
$request->query('cko-session-id')
);
if ($payment->approved) {
return $this->processSuccessfulPayment($payment);

View File

@ -12,7 +12,7 @@
namespace App\PaymentDrivers\Common;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
interface MethodInterface
{

View File

@ -14,7 +14,7 @@ namespace App\PaymentDrivers\GoCardless;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;

View File

@ -14,7 +14,7 @@ namespace App\PaymentDrivers\GoCardless;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;

View File

@ -14,7 +14,7 @@ namespace App\PaymentDrivers\GoCardless;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;

View File

@ -13,7 +13,7 @@
namespace App\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;

View File

@ -14,7 +14,7 @@ namespace App\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;

View File

@ -14,7 +14,7 @@ namespace App\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;

View File

@ -13,7 +13,7 @@
namespace App\PaymentDrivers\Mollie;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;

View File

@ -93,7 +93,7 @@ class PayPalExpressPaymentDriver extends BaseDriver
return $response->redirect();
}
$this->sendFailureMail($response->getData());
$this->sendFailureMail($response->getMessage());
$message = [
'server_response' => $response->getMessage(),

View File

@ -15,7 +15,7 @@ namespace App\PaymentDrivers\Razorpay;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;

View File

@ -13,16 +13,20 @@
namespace App\PaymentDrivers\Square;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use Illuminate\Http\Request;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\SquarePaymentDriver;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Square\Http\ApiResponse;
class CreditCard
class CreditCard implements MethodInterface
{
use MakesHash;
@ -34,90 +38,27 @@ class CreditCard
$this->square_driver->init();
}
public function authorizeView($data)
/**
* Authorization page for credit card.
*
* @param array $data
* @return View
*/
public function authorizeView($data): View
{
$data['gateway'] = $this->square_driver;
return render('gateways.square.credit_card.authorize', $data);
}
public function authorizeResponse($request)
/**
* Handle authorization for credit card.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse($request): RedirectResponse
{
/* Step one - process a $1 payment - but don't complete it*/
$payment = false;
$amount_money = new \Square\Models\Money();
$amount_money->setAmount(100); //amount in cents
$amount_money->setCurrency($this->square_driver->client->currency()->code);
$body = new \Square\Models\CreatePaymentRequest(
$request->sourceId,
Str::random(32),
$amount_money
);
$body->setAutocomplete(false);
$body->setLocationId($this->square_driver->company_gateway->getConfigField('locationId'));
$body->setReferenceId(Str::random(16));
$api_response = $this->square_driver->square->getPaymentsApi()->createPayment($body);
if ($api_response->isSuccess()) {
$result = $api_response->getBody();
$payment = json_decode($result);
} else {
$errors = $api_response->getErrors();
return $this->processUnsuccessfulPayment($errors);
}
/* Step 3 create the card */
$card = new \Square\Models\Card();
$card->setCardholderName($this->square_driver->client->present()->name());
// $card->setBillingAddress($billing_address);
$card->setCustomerId($this->findOrCreateClient());
$card->setReferenceId(Str::random(8));
$body = new \Square\Models\CreateCardRequest(
Str::random(32),
$payment->payment->id,
$card
);
$api_response = $this->square_driver
->square
->getCardsApi()
->createCard($body);
$card = false;
if ($api_response->isSuccess()) {
$card = $api_response->getBody();
$card = json_decode($card);
} else {
$errors = $api_response->getErrors();
return $this->processUnsuccessfulPayment($errors);
}
/* Create the token in Invoice Ninja*/
$cgt = [];
$cgt['token'] = $card->card->id;
$cgt['payment_method_id'] = GatewayType::CREDIT_CARD;
$payment_meta = new \stdClass;
$payment_meta->exp_month = $card->card->exp_month;
$payment_meta->exp_year = $card->card->exp_year;
$payment_meta->brand = $card->card->card_brand;
$payment_meta->last4 = $card->card->last_4;
$payment_meta->type = GatewayType::CREDIT_CARD;
$cgt['payment_meta'] = $payment_meta;
$token = $this->square_driver->storeGatewayToken($cgt, [
'gateway_customer_reference' => $this->findOrCreateClient(),
]);
return redirect()->route('client.payment_methods.index');
}
@ -170,8 +111,9 @@ class CreditCard
$body->setLocationId($this->square_driver->company_gateway->getConfigField('locationId'));
$body->setReferenceId(Str::random(16));
if($request->has('verificationToken') && $request->input('verificationToken'))
if ($request->has('verificationToken') && $request->input('verificationToken')) {
$body->setVerificationToken($request->input('verificationToken'));
}
if ($request->shouldUseToken()) {
$body->setCustomerId($cgt->gateway_customer_reference);
@ -181,66 +123,12 @@ class CreditCard
$response = $this->square_driver->square->getPaymentsApi()->createPayment($body);
if ($response->isSuccess()) {
if ($request->shouldStoreToken()) {
$this->storePaymentMethod($response);
}
return $this->processSuccessfulPayment($response);
}
return $this->processUnsuccessfulPayment($response);
}
private function storePaymentMethod(ApiResponse $response)
{
$payment = \json_decode($response->getBody());
$billing_address = new \Square\Models\Address();
$billing_address->setAddressLine1($this->square_driver->client->address1);
$billing_address->setAddressLine2($this->square_driver->client->address2);
$billing_address->setLocality($this->square_driver->client->city);
$billing_address->setAdministrativeDistrictLevel1($this->square_driver->client->state);
$billing_address->setPostalCode($this->square_driver->client->postal_code);
$billing_address->setCountry($this->square_driver->client->country->iso_3166_2);
$card = new \Square\Models\Card();
$card->setCardholderName($this->square_driver->client->present()->first_name(). " " .$this->square_driver->client->present()->last_name());
$card->setCustomerId($this->findOrCreateClient());
$card->setReferenceId(Str::random(8));
$card->setBillingAddress($billing_address);
$body = new \Square\Models\CreateCardRequest(Str::random(32), $payment->payment->id, $card);
/** @var ApiResponse */
$api_response = $this->square_driver
->square
->getCardsApi()
->createCard($body);
if (!$api_response->isSuccess()) {
return $this->processUnsuccessfulPayment($api_response);
}
$card = \json_decode($api_response->getBody());
$cgt = [];
$cgt['token'] = $card->card->id;
$cgt['payment_method_id'] = GatewayType::CREDIT_CARD;
$payment_meta = new \stdClass;
$payment_meta->exp_month = $card->card->exp_month;
$payment_meta->exp_year = $card->card->exp_year;
$payment_meta->brand = $card->card->card_brand;
$payment_meta->last4 = $card->card->last_4;
$payment_meta->type = GatewayType::CREDIT_CARD;
$cgt['payment_meta'] = $payment_meta;
$this->square_driver->storeGatewayToken($cgt, [
'gateway_customer_reference' => $this->findOrCreateClient(),
]);
}
private function processSuccessfulPayment(ApiResponse $response)
{
$body = json_decode($response->getBody());
@ -301,9 +189,9 @@ class CreditCard
$customers = $api_response->getBody();
$customers = json_decode($customers);
if(count(array($api_response->getBody(),1)) == 0)
if (count([$api_response->getBody(),1]) == 0) {
$customers = false;
}
} else {
$errors = $api_response->getErrors();
}

View File

@ -141,8 +141,8 @@ class BECS
$method = $this->stripe->getStripePaymentMethod($intent->payment_method);
$payment_meta = new \stdClass;
$payment_meta->brand = (string) \sprintf('%s (%s)', $method->sepa_debit->bank_code, ctrans('texts.becs'));
$payment_meta->last4 = (string) $method->sepa_debit->last4;
$payment_meta->brand = (string) \sprintf('%s (%s)', $method->au_becs_debit->bank_code, ctrans('texts.becs'));
$payment_meta->last4 = (string) $method->au_becs_debit->last4;
$payment_meta->state = 'authorized';
$payment_meta->type = GatewayType::BECS;

View File

@ -208,7 +208,7 @@ class StripePaymentDriver extends BaseDriver
&& $this->client->currency()
&& ($this->client->currency()->code == 'AUD')
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ["AUS", "DEU"]))
&& in_array($this->client->country->iso_3166_3, ['AUS']))
$types[] = GatewayType::BECS;
if ($this->client

View File

@ -185,7 +185,7 @@ class WePayPaymentDriver extends BaseDriver
}
if (! isset($objectType)) {
throw new Exception('Could not find object id parameter');
throw new \Exception('Could not find object id parameter');
}
if ($objectType == 'credit_card') {

View File

@ -113,25 +113,26 @@ class BaseRepository
* @param $action
*
* @return int
* @deprecated - this doesn't appear to be used anywhere?
*/
public function bulk($ids, $action)
{
if (! $ids) {
return 0;
}
// public function bulk($ids, $action)
// {
// if (! $ids) {
// return 0;
// }
$ids = $this->transformKeys($ids);
// $ids = $this->transformKeys($ids);
$entities = $this->findByPublicIdsWithTrashed($ids);
// $entities = $this->findByPublicIdsWithTrashed($ids);
foreach ($entities as $entity) {
if (auth()->user()->can('edit', $entity)) {
$this->$action($entity);
}
}
// foreach ($entities as $entity) {
// if (auth()->user()->can('edit', $entity)) {
// $this->$action($entity);
// }
// }
return count($entities);
}
// return count($entities);
// }
/* Returns an invoice if defined as a key in the $resource array*/
public function getInvitation($invitation, $resource)

View File

@ -65,7 +65,7 @@ class ClientRepository extends BaseRepository
$client->fill($data);
$client->save();
if (!isset($client->number) || empty($client->number)) {
if (!isset($client->number) || empty($client->number) || strlen($client->number) == 0) {
$client->number = $this->getNextClientNumber($client);
}

View File

@ -134,9 +134,9 @@ class InvoiceService
*
* @return InvoiceService Parent class object
*/
public function updateBalance($balance_adjustment)
public function updateBalance($balance_adjustment, bool $is_draft = false)
{
$this->invoice = (new UpdateBalance($this->invoice, $balance_adjustment))->run();
$this->invoice = (new UpdateBalance($this->invoice, $balance_adjustment, $is_draft))->run();
if ((int)$this->invoice->balance == 0) {
$this->invoice->next_send_date = null;
@ -339,6 +339,10 @@ class InvoiceService
public function removeUnpaidGatewayFees()
{
//return early if type three does not exist.
if(!collect($this->invoice->line_items)->contains('type_id', 3))
return $this;
$this->invoice->line_items = collect($this->invoice->line_items)
->reject(function ($item) {
return $item->type_id == '3';

View File

@ -33,7 +33,7 @@ class MarkSent extends AbstractService
{
/* Return immediately if status is not draft */
if ($this->invoice->fresh()->status_id != Invoice::STATUS_DRAFT) {
if ($this->invoice && $this->invoice->fresh()->status_id != Invoice::STATUS_DRAFT) {
return $this->invoice;
}
@ -47,7 +47,7 @@ class MarkSent extends AbstractService
->service()
->applyNumber()
->setDueDate()
->updateBalance($this->invoice->amount)
->updateBalance($this->invoice->amount, true)
->deletePdf()
->setReminder()
->save();

View File

@ -20,10 +20,13 @@ class UpdateBalance extends AbstractService
public $balance_adjustment;
public function __construct($invoice, $balance_adjustment)
private $is_draft;
public function __construct($invoice, $balance_adjustment, bool $is_draft)
{
$this->invoice = $invoice;
$this->balance_adjustment = $balance_adjustment;
$this->is_draft = $is_draft;
}
public function run()
@ -34,7 +37,7 @@ class UpdateBalance extends AbstractService
$this->invoice->balance += floatval($this->balance_adjustment);
if ($this->invoice->balance == 0) {
if ($this->invoice->balance == 0 && !$this->is_draft) {
$this->invoice->status_id = Invoice::STATUS_PAID;
}

View File

@ -115,15 +115,22 @@ class DeletePayment
->updatePaidToDate($net_deletable * -1)
->save();
// $paymentable_invoice->client
// ->service()
// ->updatePaidToDate($net_deletable * -1)
// ->save();
}
});
}
else {
/* If there are no invoices - then we need to still adjust the total client->paid_to_date amount*/
$this->payment
->client
->service()
->updatePaidToDate(($this->payment->amount - $this->payment->applied)*-1)
->save();
}
return $this;
}

View File

@ -267,9 +267,17 @@ class RefundPayment
// $this->credit_note->ledger()->updateCreditBalance($adjustment_amount, $ledger_string);
$client = $this->payment->client->fresh();
//$client->service()->updatePaidToDate(-1 * $this->total_refund)->save();
$client->service()->updatePaidToDate(-1 * $refunded_invoice['amount'])->save();
}
else{
//if we are refunding and no payments have been tagged, then we need to decrement the client->paid_to_date by the total refund amount.
$client = $this->payment->client->fresh();
$client->service()->updatePaidToDate(-1 * $this->total_refund)->save();
}
return $this;
}

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.3.27',
'app_tag' => '5.3.27',
'app_version' => '5.3.29',
'app_tag' => '5.3.29',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=019831a9b0c0aff43c7f",
"/css/app.css": "/css/app.css?id=df1ea83ea621533ac837",
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=f7f7b35aa3f417a3eca3",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
@ -17,7 +17,7 @@
"/js/clients/payments/mollie-credit-card.js": "/js/clients/payments/mollie-credit-card.js?id=73b66e88e2daabcd6549",
"/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js?id=c2b5f7831e1a46dd5fb2",
"/js/clients/payments/razorpay-aio.js": "/js/clients/payments/razorpay-aio.js?id=817ab3b2b94ee37b14eb",
"/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js?id=070c86b293b532c5a56c",
"/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js?id=13ea3ff41d9417ef0140",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=81c2623fc1e5769b51c7",
"/js/clients/payments/stripe-acss.js": "/js/clients/payments/stripe-acss.js?id=4a85142c085723991d28",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=665ddf663500767f1a17",
@ -36,6 +36,6 @@
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=5c35d28cf0a3286e7c45",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=2a99d83305ba87bfa6cc",
"/js/clients/statements/view.js": "/js/clients/statements/view.js?id=ca3ec4cea0de824f3a36",
"/js/setup/setup.js": "/js/setup/setup.js?id=03ea88a737e59eb2bd5a",
"/js/setup/setup.js": "/js/setup/setup.js?id=8d454e7090f119552a6c",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad"
}

View File

@ -43,57 +43,39 @@ class SquareCreditCard {
}
}
// ,
// function(err,verification) {
// if (err == null) {
// console.log("no error");
// console.log(verification);
// verificationToken = verificationResults.token;
// }
// console.log(err);
// die("verify buyer");
// }
async completePaymentWithoutToken(e) {
document.getElementById('errors').hidden = true;
e.target.parentElement.disabled = true;
let result = await this.card.tokenize();
console.log("square token = " + result.token);
/* SCA */
let verificationToken;
let verificationToken;
try {
const verificationDetails = {
amount: document.querySelector('meta[name=amount]').content,
billingContact: JSON.parse(document.querySelector('meta[name=square_contact]').content),
currencyCode: document.querySelector('meta[name=currencyCode]').content,
intent: 'CHARGE'
};
console.log(verificationDetails);
try {
const verificationDetails = {
amount: document.querySelector('meta[name=amount]').content,
billingContact: JSON.parse(
document.querySelector('meta[name=square_contact]').content
),
currencyCode: document.querySelector('meta[name=currencyCode]')
.content,
intent: 'CHARGE',
};
const verificationResults = await this.payments.verifyBuyer(
result.token,
verificationDetails
result.token,
verificationDetails
);
verificationToken = verificationResults.token;
}
catch(typeError){
console.log(typeError);
} catch (typeError) {
e.target.parentElement.disabled = true
}
console.debug('Verification Token:', verificationToken);
document.querySelector('input[name="verificationToken"]').value =
verificationToken;
document.querySelector(
'input[name="verificationToken"]'
).value = verificationToken;
if (result.status === 'OK') {
document.getElementById('sourceId').value = result.token;
@ -125,23 +107,20 @@ class SquareCreditCard {
/* SCA */
async verifyBuyer(token) {
console.log("in verify buyer");
const verificationDetails = {
amount: document.querySelector('meta[name=amount]').content,
billingContact: document.querySelector('meta[name=square_contact]').content,
currencyCode: document.querySelector('meta[name=currencyCode]').content,
intent: 'CHARGE'
amount: document.querySelector('meta[name=amount]').content,
billingContact: document.querySelector('meta[name=square_contact]')
.content,
currencyCode: document.querySelector('meta[name=currencyCode]')
.content,
intent: 'CHARGE',
};
const verificationResults = await this.payments.verifyBuyer(
token,
verificationDetails
token,
verificationDetails
);
console.log(" verification toke = " + verificationResults.token);
return verificationResults.token;
}

View File

@ -1400,7 +1400,7 @@ $LANG = array(
'more_options' => 'More options',
'credit_card' => 'Credit Card',
'bank_transfer' => 'Bank Transfer',
'no_transaction_reference' => 'We did not recieve a payment transaction reference from the gateway.',
'no_transaction_reference' => 'We did not receive a payment transaction reference from the gateway.',
'use_bank_on_file' => 'Use Bank on File',
'auto_bill_email_message' => 'This invoice will automatically be billed to the payment method on file on the due date.',
'bitcoin' => 'Bitcoin',
@ -4328,13 +4328,14 @@ $LANG = array(
'giropay_law' => 'By entering your Customer information (such as name, sort code and account number) you (the Customer) agree that this information is given voluntarily.',
'eps' => 'EPS',
'becs' => 'BECS Direct Debit',
'becs_mandate' => 'By providing your bank account details, you agree to this <a href="https://stripe.com/au-becs-dd-service-agreement/legal">Direct Debit Request and the Direct Debit Request service agreement</a>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.',
'becs_mandate' => 'By providing your bank account details, you agree to this <a class="underline" href="https://stripe.com/au-becs-dd-service-agreement/legal">Direct Debit Request and the Direct Debit Request service agreement</a>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.',
'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.',
'direct_debit' => 'Direct Debit',
'clone_to_expense' => 'Clone to expense',
'checkout' => 'Checkout',
'acss' => 'Pre-authorized debit payments',
'invalid_amount' => 'Invalid amount. Number/Decimal values only.'
'invalid_amount' => 'Invalid amount. Number/Decimal values only.',
'client_payment_failure_body' => 'Payment for Invoice :invoice for amount :amount failed.',
);
return $LANG;

View File

@ -1,37 +1,7 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_credit_card'), 'card_title'
=> ctrans('texts.payment_type_credit_card')])
@section('gateway_head')
<meta name="square-appId" content="{{ $gateway->company_gateway->getConfigField('applicationId') }}">
<meta name="square-locationId" content="{{ $gateway->company_gateway->getConfigField('locationId') }}">
<meta name="square-authorize" content="true">
@endsection
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.credit_card'), 'card_title' => ctrans('texts.credit_card')])
@section('gateway_content')
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}"
method="post" id="server_response">
@csrf
<input type="text" name="sourceId" id="sourceId" hidden>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element-single')
<div id="card-container"></div>
<div id="payment-status-container"></div>
@endcomponent
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-card'])
{{ ctrans('texts.add_payment_method') }}
{{ __('texts.payment_method_cannot_be_preauthorized') }}
@endcomponent
@endsection
@section('gateway_footer')
@if ($gateway->company_gateway->getConfigField('testMode'))
<script type="text/javascript" src="https://sandbox.web.squarecdn.com/v1/square.js"></script>
@else
<script type="text/javascript" src="https://web.squarecdn.com/v1/square.js"></script>
@endif
<script src="{{ asset('js/clients/payments/square-credit-card.js') }}"></script>
@endsection

View File

@ -12,17 +12,20 @@
<label for="becs-name">
<input class="input w-full" id="becs-name" type="text" placeholder="{{ ctrans('texts.bank_account_holder') }}" required>
</label>
<label for="becs-email" >
<label for="becs-email">
<input class="input w-full" id="becs-email-address" type="email" placeholder="{{ ctrans('texts.email') }}" required>
</label>
<label>
<div class="border p-4 rounded">
<div class="border p-4 rounded mt-2">
<div id="becs-iban"></div>
</div>
</label>
<div id="mandate-acceptance">
<div id="mandate-acceptance" class="mt-2">
<input type="checkbox" id="becs-mandate-acceptance" class="input mr-4">
<label for="becs-mandate-acceptance">{{ctrans('texts.becs_mandat')}}</label>
<label for="becs-mandate-acceptance">{!! ctrans('texts.becs_mandate') !!}</label>
</div>
</form>
@endcomponent

View File

@ -0,0 +1,103 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace Tests\Feature\Payments;
use App\Models\Payment;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\WithoutEvents;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\MockUnitData;
use Tests\TestCase;
/**
* @test
*/
class UnappliedPaymentDeleteTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockUnitData;
use WithoutEvents;
public function setUp() :void
{
parent::setUp();
$this->faker = \Faker\Factory::create();
$this->makeTestData();
$this->withoutExceptionHandling();
$this->withoutMiddleware(
ThrottleRequests::class
);
}
public function testUnappliedPaymentDelete()
{
$data = [
'amount' => 1000,
'client_id' => $this->client->hashed_id,
'invoices' => [
],
'date' => '2020/12/12',
];
$response = null;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/payments', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
$this->assertNotNull($message);
}
if ($response){
$arr = $response->json();
$response->assertStatus(200);
$payment_id = $arr['data']['id'];
$payment = Payment::with('client')->find($this->decodePrimaryKey($payment_id));
$this->assertEquals(1000, $payment->amount);
$this->assertEquals(1000, $payment->client->paid_to_date);
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->delete('/api/v1/payments/'. $payment_id);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
$this->assertNotNull($message);
}
$response->assertStatus(200);
$this->assertEquals(0, $this->client->fresh()->paid_to_date);
}
}
}

View File

@ -0,0 +1,110 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace Tests\Feature\Payments;
use App\Models\Payment;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\WithoutEvents;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\MockUnitData;
use Tests\TestCase;
/**
* @test
*/
class UnappliedPaymentRefundTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockUnitData;
use WithoutEvents;
public function setUp() :void
{
parent::setUp();
$this->faker = \Faker\Factory::create();
$this->makeTestData();
$this->withoutExceptionHandling();
$this->withoutMiddleware(
ThrottleRequests::class
);
}
public function testUnappliedPaymentRefund()
{
$data = [
'amount' => 1000,
'client_id' => $this->client->hashed_id,
'invoices' => [
],
'date' => '2020/12/12',
];
$response = null;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/payments', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
$this->assertNotNull($message);
}
if ($response){
$arr = $response->json();
$response->assertStatus(200);
$this->assertEquals(1000, $this->client->fresh()->paid_to_date);
$payment_id = $arr['data']['id'];
$this->assertEquals(1000, $arr['data']['amount']);
$payment = Payment::whereId($this->decodePrimaryKey($payment_id))->first();
$data = [
'id' => $this->encodePrimaryKey($payment->id),
'amount' => 500,
'date' => '2020/12/12',
];
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/payments/refund', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
$this->assertNotNull($message);
}
$response->assertStatus(200);
$this->assertEquals(500, $this->client->fresh()->paid_to_date);
}
}
}

View File

@ -32,6 +32,10 @@ class DownloadHistoricalInvoiceTest extends TestCase
parent::setUp();
$this->makeTestData();
if (config('ninja.testvars.travis') !== false) {
$this->markTestSkipped('Skip test for Travis');
}
}
private function mockActivity()

View File

@ -10,6 +10,7 @@
*/
namespace Tests\Unit;
use App\Factory\InvoiceItemFactory;
use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Support\Facades\Session;
use Tests\TestCase;
@ -19,13 +20,10 @@ use Tests\TestCase;
*/
class CollectionMergingTest extends TestCase
{
use UserSessionAttributes;
public function setUp() :void
{
parent::setUp();
Session::start();
}
public function testUniqueValues()
@ -62,4 +60,21 @@ class CollectionMergingTest extends TestCase
$intersect = $collection->intersectByKeys($collection->flatten(1)->unique());
$this->assertEquals(11, $intersect->count());
}
public function testExistenceInCollection()
{
$items = InvoiceItemFactory::generate(5);
$this->assertFalse(collect($items)->contains('type_id', "3"));
$this->assertFalse(collect($items)->contains('type_id', 3));
$item = InvoiceItemFactory::create();
$item->type_id = "3";
$items[] = $item;
$this->assertTrue(collect($items)->contains('type_id', "3"));
$this->assertTrue(collect($items)->contains('type_id', 3));
}
}