1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 13:12:50 +01:00

Merge pull request #8346 from turbo124/v5-develop

Fixes for FROM address
This commit is contained in:
David Bomba 2023-03-10 08:31:15 +11:00 committed by GitHub
commit 0841ecb124
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 251 additions and 77 deletions

View File

@ -1 +1 @@
5.5.84
5.5.85

View File

@ -85,7 +85,8 @@ class InvitationController extends Controller
->with('contact.client')
->firstOrFail();
if ($invitation->{$entity}->is_deleted) {
//09-03-2023 do not show entity if the invitation has been trashed.
if ($invitation->trashed() || $invitation->{$entity}->is_deleted) {
return $this->render('generic.not_available', ['account' => $invitation->company->account, 'company' => $invitation->company]);
}

View File

@ -93,7 +93,7 @@ class UpdateCompanyRequest extends Request
* are saveable
*
* @param object $settings
* @return stdClass $settings
* @return \stdClass $settings
*/
private function filterSaveableSettings($settings)
{

View File

@ -73,6 +73,11 @@ class UpdateUserRequest extends Request
$input['oauth_user_id'] = '';
}
if (array_key_exists('oauth_user_token', $input) && $input['oauth_user_token'] == '***') {
unset($input['oauth_user_token']);
}
$this->replace($input);
}
}

View File

@ -23,26 +23,20 @@ class StoreVendorRequest extends Request
/**
* Determine if the user is authorized to make this request.
*
* @return bool
* @method static \Illuminate\Contracts\Auth\Authenticatable|null user()
*/
public function authorize() : bool
{
/** @var \App\User|null $user */
$user = auth()->user();
return $user->can('create', Vendor::class);
return auth()->user()->can('create', Vendor::class);
}
public function rules()
{
/** @var \App\User|null $user */
$user = auth()->user();
$rules = [];
$rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email';
if (isset($this->number)) {
$rules['number'] = Rule::unique('vendors')->where('company_id', $user->company()->id);
$rules['number'] = Rule::unique('vendors')->where('company_id', auth()->user()->company()->id);
}
$rules['currency_id'] = 'bail|required|exists:currencies,id';
@ -63,13 +57,11 @@ class StoreVendorRequest extends Request
public function prepareForValidation()
{
/** @var \App\User|null $user */
$user = auth()->user();
$input = $this->all();
if (!array_key_exists('currency_id', $input) || empty($input['currency_id'])) {
$input['currency_id'] = $user->company()->settings->currency_id;
$input['currency_id'] = auth()->user()->company()->settings->currency_id;
}
$input = $this->decodePrimaryKeys($input);

View File

@ -24,11 +24,8 @@ class UpdateReminders implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Company $company;
public function __construct(Company $company)
public function __construct(public Company $company)
{
$this->company = $company;
}
/**

View File

@ -22,7 +22,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Support\Facades\Redis;
class AdjustEmailQuota implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -61,18 +61,47 @@ class AdjustEmailQuota implements ShouldQueue
Account::query()->cursor()->each(function ($account) {
nlog("resetting email quota for {$account->key}");
$email_count = Cache::get($account->key);
$email_count = Cache::get("email_quota".$account->key);
if ($email_count > 0) {
try {
LightLogs::create(new EmailCount($email_count, $account->key))->send();
LightLogs::create(new EmailCount($email_count, $account->key))->send(); // this runs syncronously
} catch(\Exception $e) {
nlog($e->getMessage());
}
}
Cache::forget($account->key);
Cache::forget("throttle_notified:{$account->key}");
});
/** Use redis pipelines to execute bulk deletes efficiently */
$redis = Redis::connection('sentinel-cache');
$prefix = config('cache.prefix'). ":email_quota*";
$keys = $redis->keys($prefix);
if(is_array($keys))
{
$redis->pipeline(function ($pipe) use ($keys) {
foreach ($keys as $key) {
$pipe->del($key);
}
});
}
$keys = null;
$prefix = config('cache.prefix'). ":throttle_notified*";
$keys = $redis->keys($prefix);
if (is_array($keys)) {
$redis->pipeline(function ($pipe) use ($keys) {
foreach ($keys as $key) {
$pipe->del($key);
}
});
}
}
}

View File

@ -26,8 +26,6 @@ class CleanStaleInvoiceOrder implements ShouldQueue
/**
* Create a new job instance.
*
* @param int invoice_id
* @param string $db
*/
public function __construct()
{

View File

@ -339,7 +339,8 @@ class Account extends BaseModel
return false;
}
if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now())) {
// 09-03-2023 - winds forward expiry checks to ensure we don't cut off users prior to billing cycle being commenced
if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()->subHours(12))) {
return false;
}
@ -352,7 +353,7 @@ class Account extends BaseModel
return false;
}
if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now())) {
if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()->subHours(12))) {
return true;
}
@ -526,12 +527,12 @@ class Account extends BaseModel
public function emailQuotaExceeded() :bool
{
if (is_null(Cache::get($this->key))) {
if (is_null(Cache::get("email_quota".$this->key))) {
return false;
}
try {
if (Cache::get($this->key) > $this->getDailyEmailLimit()) {
if (Cache::get("email_quota".$this->key) > $this->getDailyEmailLimit()) {
if (is_null(Cache::get("throttle_notified:{$this->key}"))) {
App::forgetInstance('translator');
$t = app('translator');

19
app/Models/License.php Normal file
View File

@ -0,0 +1,19 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
class License extends StaticModel
{
use SoftDeletes;
}

View File

@ -182,8 +182,8 @@ class User extends Authenticatable implements MustVerifyEmail
'accepted_terms_version',
'oauth_user_id',
'oauth_provider_id',
'oauth_user_token',
'oauth_user_refresh_token',
// 'oauth_user_token',
// 'oauth_user_refresh_token',
'custom_value1',
'custom_value2',
'custom_value3',

View File

@ -356,6 +356,20 @@ class ACH
$response = json_decode($request->gateway_response);
$bank_account_response = json_decode($request->bank_account_response);
if($response->status == 'requires_source_action' && $response->next_action->type == 'verify_with_microdeposits')
{
$method = $bank_account_response->payment_method->us_bank_account;
$method = $bank_account_response->payment_method->us_bank_account;
$method->id = $response->payment_method;
$method->state = 'unauthorized';
$method->next_action = $response->next_action->verify_with_microdeposits->hosted_verification_url;
$customer = $this->stripe->getCustomer($request->customer);
$cgt = $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer);
return redirect()->route('client.payment_methods.show', ['payment_method' => $cgt->hashed_id]);
}
$method = $bank_account_response->payment_method->us_bank_account;
$method->id = $response->payment_method;
$method->state = 'authorized';
@ -547,6 +561,10 @@ class ACH
$payment_meta->type = GatewayType::BANK_TRANSFER;
$payment_meta->state = $state;
if(property_exists($method, 'next_action')) {
$payment_meta->next_action = $method->next_action;
}
$data = [
'payment_meta' => $payment_meta,
'token' => $method->id,

View File

@ -78,8 +78,19 @@ class PaymentIntentProcessingWebhook implements ShouldQueue
$this->payment_completed = true;
}
}
if(isset($transaction['payment_method']))
{
$cgt = ClientGatewayToken::where('token', $transaction['payment_method'])->first();
if($cgt && $cgt->meta?->state == 'unauthorized'){
$meta = $cgt->meta;
$meta->state = 'authorized';
$cgt->meta = $meta;
$cgt->save();
}
}
}
if ($this->payment_completed) {
return;

View File

@ -90,15 +90,21 @@ class UpdatePaymentMethods
);
foreach ($bank_methods->data as $method) {
$token_exists = ClientGatewayToken::where([
$token = ClientGatewayToken::where([
'gateway_customer_reference' => $customer->id,
'token' => $method->id,
'client_id' => $client->id,
'company_id' => $client->company_id,
])->exists();
])->first();
/* Already exists return */
if ($token_exists) {
if ($token) {
$meta = $token->meta;
$meta->state = 'authorized';
$token->meta = $meta;
$token->save();
continue;
}

View File

@ -11,16 +11,20 @@
namespace App\Providers;
use App\Utils\Ninja;
use App\Models\Scheduler;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Route;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
use MakesHash;
private int $default_rate_limit = 5000;
/**
* Define your route model bindings, pattern filters, etc.
*
@ -40,6 +44,37 @@ class RouteServiceProvider extends ServiceProvider
->company()
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
});
RateLimiter::for('login', function () {
if (Ninja::isSelfHost()) {
return Limit::none();
}else {
return Limit::perMinute(50);
}
});
RateLimiter::for('api', function () {
if (Ninja::isSelfHost()) {
return Limit::none();
}else {
return Limit::perMinute(300);
}
});
RateLimiter::for('refresh', function () {
if (Ninja::isSelfHost()) {
return Limit::none();
}else {
return Limit::perMinute(200);
}
});
}
/**

View File

@ -134,7 +134,7 @@ class EmailDefaults
return $this;
}
$this->email->email_object->from = new Address($this->email->company->owner()->email, $this->email->company->owner()->name());
$this->email->email_object->from = new Address(config('mail.from.address'), config('mail.from.name'));
return $this;
}

View File

@ -11,38 +11,42 @@
namespace App\Services\Subscription;
use App\DataMapper\InvoiceItem;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
use App\Mail\RecurringInvoice\ClientContactRequestCancellationObject;
use Carbon\Carbon;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\License;
use App\Models\Product;
use App\Models\SystemLog;
use App\Libraries\MultiDB;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\Product;
use App\Models\RecurringInvoice;
use Illuminate\Support\Str;
use App\Models\Subscription;
use App\Models\SystemLog;
use App\Models\ClientContact;
use App\Services\Email\Email;
use App\Factory\CreditFactory;
use App\Jobs\Mail\NinjaMailer;
use App\DataMapper\InvoiceItem;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Models\RecurringInvoice;
use App\Jobs\Mail\NinjaMailerJob;
use App\Services\Email\EmailObject;
use App\Jobs\Mail\NinjaMailerObject;
use App\Utils\Traits\CleanLineItems;
use App\Repositories\CreditRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\PaymentRepository;
use App\Repositories\RecurringInvoiceRepository;
use App\Repositories\SubscriptionRepository;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Notifications\UserNotifies;
use App\Factory\RecurringInvoiceFactory;
use App\Utils\Traits\SubscriptionHooker;
use Carbon\Carbon;
use App\Repositories\SubscriptionRepository;
use App\Repositories\RecurringInvoiceRepository;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Contracts\Container\BindingResolutionException;
use App\Mail\RecurringInvoice\ClientContactRequestCancellationObject;
class SubscriptionService
{
@ -54,6 +58,8 @@ class SubscriptionService
/** @var subscription */
private $subscription;
private const WHITE_LABEL = 4316;
private float $credit_payments = 0;
public function __construct(Subscription $subscription)
@ -75,6 +81,11 @@ class SubscriptionService
return $this->handlePlanChange($payment_hash);
}
if ($payment_hash->data->billing_context->context == 'whitelabel') {
return $this->handleWhiteLabelPurchase($payment_hash);
}
// if we have a recurring product - then generate a recurring invoice
if (strlen($this->subscription->recurring_product_ids) >=1) {
if (isset($payment_hash->data->billing_context->bundle)) {
@ -153,6 +164,45 @@ class SubscriptionService
return $response;
}
private function handleWhiteLabelPurchase(PaymentHash $payment_hash): bool
{
//send license to the user.
$invoice = $payment_hash->fee_invoice;
$license_key = Str::uuid()->toString();
$invoice->public_notes = $license_key;
$invoice->save();
$invoice->service()->touchPdf();
$contact = $invoice->client->contacts()->whereNotNull('email')->first();
$license = new License;
$license->license_key = $license_key;
$license->email = $contact ? $contact->email : ' ';
$license->first_name = $contact ? $contact->first_name : ' ';
$license->last_name = $contact ? $contact->last_name : ' ';
$license->is_claimed = 1;
$license->transaction_reference = $payment_hash?->payment?->transaction_reference ?: ' ';
$license->product_id = self::WHITE_LABEL;
$license->save();
$email_object = new EmailObject;
$email_object->to = $contact->email;
$email_object->subject = ctrans('texts.white_label_link') . " " .ctrans('texts.payment_subject');
$email_object->body = ctrans('texts.white_label_body',['license_key' => $license_key]);
$email_object->client_id = $invoice->client_id;
$email_object->client_contact_id = $contact->id;
$email_object->invitation_key = $invoice->invitations()->first()->invitation_key;
$email_object->entity_id = $invoice->id;
$email_object->entity_class = Invoice::class;
$email_object->user_id = $invoice->user_id;
Email::dispatch($email_object, $invoice->company);
return true;
}
/* Starts the process to create a trial
- we create a recurring invoice, which is has its next_send_date as now() + trial_duration
- we then hit the client API end point to advise the trial payload

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.5.84',
'app_tag' => '5.5.84',
'app_version' => '5.5.85',
'app_tag' => '5.5.85',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -5014,6 +5014,8 @@ $LANG = array(
'no_assigned_tasks' => 'No billable tasks for this project',
'authorization_failure' => 'Insufficient permissions to perform this action',
'authorization_sms_failure' => 'Please verify your account to send emails.',
'white_label_body' => 'Thank you for purchasing a white label license. Your license key is :license_key.',
);

View File

@ -2,4 +2,4 @@ parameters:
level: 2
paths:
- app
- tests
# - tests

View File

@ -1,6 +1,6 @@
@php
$primary_color = isset($settings) ? $settings->primary_color : '#4caf50';
$email_alignment = isset($settings) ? $settings->email_alignment : 'center';
$email_alignment = isset($settings) && $settings?->email_alignment ? $settings->email_alignment : 'center';
@endphp
<!DOCTYPE html

View File

@ -213,6 +213,10 @@
errors.textContent = "You will receive an email with details on how to verify your bank account and process payment.";
errors.hidden = false;
document.getElementById('new-bank').style.visibility = 'hidden'
let gateway_response = document.getElementById('gateway_response');
gateway_response.value = JSON.stringify(paymentIntent);
document.getElementById('server-response').submit();
}
});

View File

@ -100,9 +100,15 @@
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }">
@if (substr($payment_method->token, 0, 2) === 'pm')
<a href="{{ $payment_method->meta?->next_action }}" class="button button-primary bg-primary">
{{ ctrans('texts.complete_verification') }}
</a>
@else
<a href="{{ route('client.payment_methods.verification', ['payment_method' => $payment_method->hashed_id, 'method' => \App\Models\GatewayType::BANK_TRANSFER]) }}" class="button button-primary bg-primary">
{{ ctrans('texts.complete_verification') }}
</a>
@endif
</div>
</div>
</div>

View File

@ -98,17 +98,17 @@ use App\Http\Controllers\WebCronController;
use App\Http\Controllers\WebhookController;
use Illuminate\Support\Facades\Route;
Route::group(['middleware' => ['throttle:300,1', 'api_secret_check']], function () {
Route::group(['middleware' => ['throttle:api', 'api_secret_check']], function () {
Route::post('api/v1/signup', [AccountController::class, 'store'])->name('signup.submit');
Route::post('api/v1/oauth_login', [LoginController::class, 'oauthApiLogin']);
});
Route::group(['middleware' => ['throttle:50,1','api_secret_check','email_db']], function () {
Route::post('api/v1/login', [LoginController::class, 'apiLogin'])->name('login.submit')->middleware('throttle:20,1');
Route::group(['middleware' => ['throttle:login','api_secret_check','email_db']], function () {
Route::post('api/v1/login', [LoginController::class, 'apiLogin'])->name('login.submit');
Route::post('api/v1/reset_password', [ForgotPasswordController::class, 'sendResetLinkEmail']);
});
Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () {
Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () {
Route::put('accounts/{account}', [AccountController::class, 'update'])->name('account.update');
Route::resource('bank_integrations', BankIntegrationController::class); // name = (clients. index / create / show / update / destroy / edit
Route::post('bank_integrations/refresh_accounts', [BankIntegrationController::class, 'refreshAccounts'])->name('bank_integrations.refresh_accounts')->middleware('throttle:30,1');
@ -265,7 +265,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale
Route::post('recurring_quotes/bulk', [RecurringQuoteController::class, 'bulk'])->name('recurring_quotes.bulk');
Route::put('recurring_quotes/{recurring_quote}/upload', [RecurringQuoteController::class, 'upload']);
Route::post('refresh', [LoginController::class, 'refresh'])->middleware('throttle:300,2');
Route::post('refresh', [LoginController::class, 'refresh'])->middleware('throttle:refresh');
Route::post('reports/clients', ClientReportController::class);
Route::post('reports/contacts', ClientContactReportController::class);
@ -364,7 +364,7 @@ Route::match(['get', 'post'], 'payment_notification_webhook/{company_key}/{compa
->name('payment_notification_webhook');
Route::post('api/v1/postmark_webhook', [PostMarkController::class, 'webhook'])->middleware('throttle:1000,1');
Route::get('token_hash_router', [OneTimeTokenController::class, 'router'])->middleware('throttle:100,1');
Route::get('token_hash_router', [OneTimeTokenController::class, 'router'])->middleware('throttle:500,1');
Route::get('webcron', [WebCronController::class, 'index'])->middleware('throttle:100,1');
Route::post('api/v1/get_migration_account', [HostedMigrationController::class, 'getAccount'])->middleware('guest')->middleware('throttle:100,1');
Route::post('api/v1/confirm_forwarding', [HostedMigrationController::class, 'confirmForwarding'])->middleware('guest')->middleware('throttle:100,1');

View File

@ -78,16 +78,16 @@ class AccountEmailQuotaTest extends TestCase
$account->save();
Cache::put($account->key, 3000);
Cache::put("email_quota".$account->key, 3000);
$this->assertFalse($account->isPaid());
$this->assertTrue(Ninja::isNinja());
$this->assertEquals(20, $account->getDailyEmailLimit());
$this->assertEquals(3000, Cache::get($account->key));
$this->assertEquals(3000, Cache::get("email_quota".$account->key));
$this->assertTrue($account->emailQuotaExceeded());
Cache::forget('123ifyouknowwhatimean');
Cache::forget("email_quota".'123ifyouknowwhatimean');
}
public function testQuotaValidRule()
@ -104,11 +104,11 @@ class AccountEmailQuotaTest extends TestCase
$account->num_users = 3;
$account->save();
Cache::increment($account->key);
Cache::increment("email_quota".$account->key);
$this->assertFalse($account->emailQuotaExceeded());
Cache::forget('123ifyouknowwhatimean');
Cache::forget("email_quota".'123ifyouknowwhatimean');
}
public function testEmailSentCount()
@ -132,6 +132,6 @@ class AccountEmailQuotaTest extends TestCase
$this->assertEquals(3000, $count);
Cache::forget('123ifyouknowwhatimean');
Cache::forget("email_quota".'123ifyouknowwhatimean');
}
}