Merge pull request #6879 from turbo124/v5-develop

Refactor Refunds
This commit is contained in:
David Bomba 2021-10-22 07:06:20 +11:00 committed by GitHub
commit b915cc32c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 466 additions and 47 deletions

View File

@ -90,6 +90,10 @@ class CreateSingleAccount extends Command
*/
public function handle()
{
if(config('ninja.is_docker'))
return;
MultiDB::setDb($this->option('database'));
$this->info(date('r').' Create Single Sample Account...');

View File

@ -78,6 +78,9 @@ class CreateTestData extends Command
*/
public function handle()
{
if(config('ninja.is_docker'))
return;
$this->info(date('r').' Running CreateTestData...');
$this->count = $this->argument('count');

View File

@ -86,6 +86,9 @@ class DemoMode extends Command
{
set_time_limit(0);
if(config('ninja.is_docker'))
return;
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {

View File

@ -59,37 +59,6 @@ class SubdomainFill extends Command
});
// $db1 = Company::on('db-ninja-01')->get();
// $db1->each(function ($company){
// $db2 = Company::on('db-ninja-02a')->find($company->id);
// if($db2)
// {
// $db2->subdomain = $company->subdomain;
// $db2->save();
// }
// });
// $db1 = null;
// $db2 = null;
// $db2 = Company::on('db-ninja-02')->get();
// $db2->each(function ($company){
// $db1 = Company::on('db-ninja-01a')->find($company->id);
// if($db1)
// {
// $db1->subdomain = $company->subdomain;
// $db1->save();
// }
// });
}
}

View File

@ -32,7 +32,7 @@ class ClientRegistrationFields
],
[
'key' => 'phone',
'required' => true
'required' => false
],
[
'key' => 'password',

View File

@ -0,0 +1,144 @@
<?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\Helpers\Invoice;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use Carbon\Exceptions\InvalidFormatException;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Carbon;
class ProRata
{
/**
* Returns the amount to refund based on
* the time interval and the frequency duration
*
* @param float $amount
* @param Carbon $from_date
* @param Carbon $to_date
* @param int $frequency
* @return float
*/
public function refund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float
{
$days = $from_date->copy()->diffInDays($to_date);
$days_in_frequency = $this->getDaysInFrequency($frequency);
return round( (($days/$days_in_frequency) * $amount),2);
}
/**
* Returns the amount to charge based on
* the time interval and the frequency duration
*
* @param float $amount
* @param Carbon $from_date
* @param Carbon $to_date
* @param int $frequency
* @return float
*/
public function charge(float $amount, Carbon $from_date, Carbon $to_date, int $frequency) :float
{
$days = $from_date->copy()->diffInDays($to_date);
$days_in_frequency = $this->getDaysInFrequency($frequency);
nlog($from_date->format('Y-m-d'));
nlog($days);
nlog($days_in_frequency);
nlog($amount);
return round( (($days/$days_in_frequency) * $amount),2);
}
/**
* Prepares the line items of an invoice
* to be pro rata refunded.
*
* @param Invoice $invoice
* @param bool $is_credit
* @return array
* @throws Exception
*/
public function refundItems(Invoice $invoice, $is_credit = false) :array
{
if(!$invoice)
return [];
$recurring_invoice = RecurringInvoice::find($invoice->recurring_id)->first();
if(!$recurring_invoice)
throw new \Exception("Invoice isn't attached to a recurring invoice");
/* depending on whether we are creating an invoice or a credit*/
$multiplier = $is_credit ? 1 : -1;
$start_date = Carbon::parse($invoice->date);
$line_items = [];
foreach($invoice->line_items as $item)
{
if($item->product_key != ctrans('texts.refund'))
{
$item->quantity = 1;
$item->cost = $this->refund($item->cost*$multiplier, $start_date, now(), $recurring_invoice->frequency_id);
$item->product_key = ctrans('texts.refund');
$item->notes = ctrans('texts.refund') . ": ". $item->notes;
$line_items[] = $item;
}
}
return $line_items;
}
private function getDaysInFrequency($frequency)
{
switch ($frequency) {
case RecurringInvoice::FREQUENCY_DAILY:
return 1;
case RecurringInvoice::FREQUENCY_WEEKLY:
return 7;
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
return 14;
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
return now()->diffInDays(now()->addWeeks(4));
case RecurringInvoice::FREQUENCY_MONTHLY:
return now()->diffInDays(now()->addMonthNoOverflow());
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
return now()->diffInDays(now()->addMonthNoOverflow(2));
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
return now()->diffInDays(now()->addMonthNoOverflow(3));
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
return now()->diffInDays(now()->addMonthNoOverflow(4));
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
return now()->diffInDays(now()->addMonthNoOverflow(6));
case RecurringInvoice::FREQUENCY_ANNUALLY:
return now()->diffInDays(now()->addYear());
case RecurringInvoice::FREQUENCY_TWO_YEARS:
return now()->diffInDays(now()->addYears(2));
case RecurringInvoice::FREQUENCY_THREE_YEARS:
return now()->diffInDays(now()->addYears(3));
default:
return 0;
}
}
}

View File

@ -0,0 +1,102 @@
<?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\Helpers\Subscription;
use App\Helpers\Invoice\ProRata;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Models\Subscription;
use Illuminate\Support\Carbon;
/**
* SubscriptionCalculator.
*/
class SubscriptionCalculator
{
public Subscription $target_subscription;
public Invoice $invoice;
public function __construct(Subscription $target_subscription, Invoice $invoice)
{
$this->target_subscription = $target_subscription;
$this->invoice = $invoice;
}
/**
* Tests if the user is currently up
* to date with their payments for
* a given recurring invoice
*
* @return bool
*/
public function isPaidUp() :bool
{
$outstanding_invoices_exist = Invoice::whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('subscription_id', $this->invoice->subscription_id)
->where('client_id', $this->invoice->client_id)
->where('balance', '>', 0)
->exists();
return ! $outstanding_invoices_exist;
}
public function calcUpgradePlan()
{
//set the starting refund amount
$refund_amount = 0;
$refund_invoice = false;
//are they paid up to date.
//yes - calculate refund
if($this->isPaidUp())
$refund_invoice = $this->getRefundInvoice();
if($refund_invoice)
{
$subscription = Subscription::find($this->invoice->subscription_id);
$pro_rata = new ProRata;
$to_date = $subscription->service()->getNextDateForFrequency(Carbon::parse($refund_invoice->date), $subscription->frequency_id);
$refund_amount = $pro_rata->refund($refund_invoice->amount, now(), $to_date, $subscription->frequency_id);
$charge_amount = $pro_rata->charge($this->target_subscription->price, now(), $to_date, $this->target_subscription->frequency_id);
return ($charge_amount - $refund_amount);
}
//no - return full freight charge.
return $this->target_subscription->price;
}
public function executeUpgradePlan()
{
}
private function getRefundInvoice()
{
return Invoice::where('subscription_id', $this->invoice->subscription_id)
->where('client_id', $this->invoice->client_id)
->where('is_deleted', 0)
->orderBy('id', 'desc')
->first();
}
}

View File

@ -260,14 +260,14 @@ class LoginController extends BaseController
->increment()
->batch();
SystemLogger::dispatch(
json_encode(['ip' => request()->getClientIp()]),
SystemLog::CATEGORY_SECURITY,
SystemLog::EVENT_USER,
SystemLog::TYPE_LOGIN_FAILURE,
null,
Company::first(),
);
// SystemLogger::dispatch(
// json_encode(['ip' => request()->getClientIp()]),
// SystemLog::CATEGORY_SECURITY,
// SystemLog::EVENT_USER,
// SystemLog::TYPE_LOGIN_FAILURE,
// null,
// Company::first(),
// );
$this->incrementLoginAttempts($request);

View File

@ -499,7 +499,7 @@ class CompanyController extends BaseController
$account->delete();
if(Ninja::isHosted() && $request->has('cancellation_message') && strlen($request->input('cancellation_message')) > 1)
if(Ninja::isHosted())
\Modules\Admin\Jobs\Account\NinjaDeletedAccount::dispatch($account_key, $request->all());
LightLogs::create(new AccountDeleted())

View File

@ -20,6 +20,7 @@ use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Utils\CurlUtils;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\Ninja;
use App\Utils\SystemHealth;
use App\Utils\Traits\AppSetup;
@ -245,7 +246,7 @@ class SetupController extends Controller
public function checkPdf(Request $request)
{
try {
if (config('ninja.phantomjs_pdf_generation')) {
if (config('ninja.pdf_generator') == 'phantom') {
return $this->testPhantom();
}

View File

@ -934,6 +934,38 @@ class SubscriptionService
}
public function getNextDateForFrequency($date, $frequency)
{
switch ($frequency) {
case RecurringInvoice::FREQUENCY_DAILY:
return $date->addDay();
case RecurringInvoice::FREQUENCY_WEEKLY:
return $date->addDays(7);
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
return $date->addDays(13);
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
return $date->addWeeks(4);
case RecurringInvoice::FREQUENCY_MONTHLY:
return $date->addMonthNoOverflow();
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
return $date->addMonthNoOverflow(2);
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
return $date->addMonthNoOverflow(3);
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
return $date->addMonthNoOverflow(4);
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
return $date->addMonthNoOverflow(6);
case RecurringInvoice::FREQUENCY_ANNUALLY:
return $date->addYear();
case RecurringInvoice::FREQUENCY_TWO_YEARS:
return $date->addYears(2);
case RecurringInvoice::FREQUENCY_THREE_YEARS:
return $date->addYears(3);
default:
return 0;
}
}
/**
* 'email' => $this->email ?? $this->contact->email,

View File

@ -25,17 +25,13 @@ use Illuminate\Support\Facades\Queue;
class SystemHealth
{
private static $extensions = [
// 'mysqli',
'gd',
'curl',
'zip',
// 'gmp',
'openssl',
'mbstring',
'xml',
'bcmath',
// 'mysqlnd',
//'intl', //todo double check whether we need this for email dns validation
];
private static $php_version = 7.4;

View File

@ -12,6 +12,7 @@
namespace Database\Factories;
use App\Models\RecurringInvoice;
use App\Models\Subscription;
use Illuminate\Database\Eloquent\Factories\Factory;
@ -32,7 +33,8 @@ class SubscriptionFactory extends Factory
public function definition()
{
return [
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
'name' => $this->faker->company(),
];
}
}

View File

@ -0,0 +1,57 @@
<?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\Unit;
use App\Helpers\Invoice\ProRata;
use App\Models\RecurringInvoice;
use App\Utils\Ninja;
use Illuminate\Support\Carbon;
use Tests\TestCase;
/**
* @test
*/
class RefundUnitTest extends TestCase
{
public function setUp() :void
{
parent::setUp();
}
public function testProRataRefundMonthly()
{
$pro_rata = new ProRata();
$refund = $pro_rata->refund(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_MONTHLY);
$this->assertEquals(9.68, $refund);
$this->assertEquals(30, Carbon::parse('2021-01-01')->diffInDays(Carbon::parse('2021-01-31')));
}
public function testProRataRefundYearly()
{
$pro_rata = new ProRata();
$refund = $pro_rata->refund(10, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-31'), RecurringInvoice::FREQUENCY_ANNUALLY);
$this->assertEquals(0.82, $refund);
}
public function testDiffInDays()
{
$this->assertEquals(30, Carbon::parse('2021-01-01')->diffInDays(Carbon::parse('2021-01-31')));
}
}

View File

@ -0,0 +1,106 @@
<?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 Tests\Unit;
use App\Helpers\Invoice\ProRata;
use App\Helpers\Subscription\SubscriptionCalculator;
use App\Models\Invoice;
use App\Models\Subscription;
use Illuminate\Support\Carbon;
use Tests\MockUnitData;
use Tests\TestCase;
/**
* @test
*/
class SubscriptionsCalcTest extends TestCase
{
use MockUnitData;
/**
* Important consideration with Base64
* encoding checks.
*
* No method can guarantee against false positives.
*/
public function setUp(): void
{
parent::setUp();
$this->makeTestData();
}
public function testCalcUpgradePrice()
{
$subscription = Subscription::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'price' => 10,
]);
$target = Subscription::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'price' => 20,
]);
$invoice = Invoice::factory()->create([
'line_items' => $this->buildLineItems(),
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_name2' => '',
'tax_rate3' => 0,
'tax_name3' => '',
'discount' => 0,
'subscription_id' => $subscription->id,
'date' => '2021-01-01',
]);
$invoice = $invoice->calc()->getInvoice();
$this->assertEquals(10, $invoice->amount);
$invoice->service()->markSent()->save();
$this->assertEquals(10, $invoice->amount);
$this->assertEquals(10, $invoice->balance);
$sub_calculator = new SubscriptionCalculator($target->fresh(), $invoice->fresh());
$this->assertFalse($sub_calculator->isPaidUp());
$invoice->service()->markPaid()->save();
$this->assertTrue($sub_calculator->isPaidUp());
$this->assertEquals(10, $invoice->amount);
$this->assertEquals(0, $invoice->balance);
$pro_rata = new ProRata;
$refund = $pro_rata->refund($invoice->amount, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-06'), $subscription->frequency_id);
$this->assertEquals(1.61, $refund);
$pro_rata = new ProRata;
$upgrade = $pro_rata->charge($target->price, Carbon::parse('2021-01-01'), Carbon::parse('2021-01-06'), $subscription->frequency_id);
$this->assertEquals(3.23, $upgrade);
}
}