mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Merge pull request #9286 from turbo124/v5-develop
Adjustments for task imports
This commit is contained in:
commit
958e0bbfc6
83
app/Console/Commands/EncryptNinja.php
Normal file
83
app/Console/Commands/EncryptNinja.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?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\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class EncryptNinja extends Command
|
||||
{
|
||||
protected $files = [
|
||||
'resources/views/email/template/admin_premium.blade.php',
|
||||
'resources/views/email/template/client_premium.blade.php',
|
||||
];
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ninja:crypt {--encrypt} {--decrypt}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Encrypt Protected files';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if($this->option('encrypt'))
|
||||
return $this->encryptFiles();
|
||||
|
||||
if($this->option('decrypt')) {
|
||||
return $this->decryptFiles();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function encryptFiles()
|
||||
{
|
||||
foreach ($this->files as $file) {
|
||||
$contents = Storage::disk('base')->get($file);
|
||||
$encrypted = encrypt($contents);
|
||||
Storage::disk('base')->put($file.".enc", $encrypted);
|
||||
Storage::disk('base')->delete($file);
|
||||
}
|
||||
}
|
||||
|
||||
private function decryptFiles()
|
||||
{
|
||||
foreach ($this->files as $file) {
|
||||
$encrypted_file = "{$file}.enc";
|
||||
$contents = Storage::disk('base')->get($encrypted_file);
|
||||
$decrypted = decrypt($contents);
|
||||
Storage::disk('base')->put($file, $decrypted);
|
||||
}
|
||||
}
|
||||
}
|
@ -144,7 +144,10 @@ class PaymentMethodController extends Controller
|
||||
try {
|
||||
event(new MethodDeleted($payment_method, auth()->guard('contact')->user()->company, Ninja::eventVars(auth()->guard('contact')->user()->id)));
|
||||
|
||||
$payment_method->is_deleted = true;
|
||||
$payment_method->delete();
|
||||
$payment_method->save();
|
||||
|
||||
} catch (Exception $e) {
|
||||
nlog($e->getMessage());
|
||||
|
||||
|
@ -74,6 +74,7 @@ class TokenAuth
|
||||
*/
|
||||
app('queue')->createPayloadUsing(function () use ($company_token) {
|
||||
return ['db' => $company_token->company->db];
|
||||
// return ['db' => $company_token->company->db, 'is_premium' => $company_token->account->isPremium()];
|
||||
});
|
||||
|
||||
//user who once existed, but has been soft deleted
|
||||
|
@ -31,9 +31,9 @@ class TaskMap
|
||||
12 => 'task.duration',
|
||||
13 => 'task.status',
|
||||
14 => 'task.custom_value1',
|
||||
15 => 'task.custom_value1',
|
||||
16 => 'task.custom_value1',
|
||||
17 => 'task.custom_value1',
|
||||
15 => 'task.custom_value2',
|
||||
16 => 'task.custom_value3',
|
||||
17 => 'task.custom_value4',
|
||||
18 => 'task.notes',
|
||||
];
|
||||
}
|
||||
|
@ -115,11 +115,22 @@ class TaskTransformer extends BaseTransformer
|
||||
$this->stubbed_timestamp = $stub_start_date->timestamp;
|
||||
|
||||
return $stub_start_date->timestamp;
|
||||
} catch (\Exception $e) {
|
||||
nlog("fall back failed too" . $e->getMessage());
|
||||
// return $this->stubbed_timestamp;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
|
||||
$stub_start_date = \Carbon\Carbon::createFromFormat($this->company->date_format(), $stub_start_date);
|
||||
$this->stubbed_timestamp = $stub_start_date->timestamp;
|
||||
} catch (\Exception $e) {
|
||||
nlog($e->getMessage());
|
||||
return $this->stubbed_timestamp;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private function resolveEndDate($item)
|
||||
@ -142,9 +153,23 @@ class TaskTransformer extends BaseTransformer
|
||||
} catch (\Exception $e) {
|
||||
nlog($e->getMessage());
|
||||
|
||||
// return $this->stubbed_timestamp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
|
||||
$stub_end_date = \Carbon\Carbon::createFromFormat($this->company->date_format(), $stub_end_date);
|
||||
$this->stubbed_timestamp = $stub_end_date->timestamp;
|
||||
} catch (\Exception $e) {
|
||||
nlog("fall back failed too" . $e->getMessage());
|
||||
return $this->stubbed_timestamp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private function getTaskStatusId($item): ?int
|
||||
|
@ -34,6 +34,7 @@ class NinjaMailer extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
|
||||
$from_name = config('mail.from.name');
|
||||
|
||||
if (property_exists($this->mail_obj, 'from_name')) {
|
||||
|
@ -294,6 +294,11 @@ class Account extends BaseModel
|
||||
return Ninja::isNinja() ? ($this->isPaidHostedClient() && !$this->isTrial()) : $this->hasFeature(self::FEATURE_WHITE_LABEL);
|
||||
}
|
||||
|
||||
public function isPremium(): bool
|
||||
{
|
||||
return Ninja::isHosted() && $this->isPaidHostedClient() && !$this->isTrial() && Carbon::createFromTimestamp($this->created_at)->diffInMonths() > 2;
|
||||
}
|
||||
|
||||
public function isPaidHostedClient(): bool
|
||||
{
|
||||
if (!Ninja::isNinja()) {
|
||||
|
@ -11,7 +11,9 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\Subscription\PaymentLinkService;
|
||||
use App\Services\Subscription\SubscriptionService;
|
||||
use App\Services\Subscription\SubscriptionStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@ -121,6 +123,8 @@ class Subscription extends BaseModel
|
||||
'updated_at' => 'timestamp',
|
||||
'created_at' => 'timestamp',
|
||||
'deleted_at' => 'timestamp',
|
||||
'trial_enabled' => 'boolean',
|
||||
'allow_plan_changes' => 'boolean',
|
||||
];
|
||||
|
||||
protected $with = [
|
||||
@ -132,6 +136,16 @@ class Subscription extends BaseModel
|
||||
return new SubscriptionService($this);
|
||||
}
|
||||
|
||||
public function link_service(): PaymentLinkService
|
||||
{
|
||||
return new PaymentLinkService($this);
|
||||
}
|
||||
|
||||
public function status(RecurringInvoice $recurring_invoice): SubscriptionStatus
|
||||
{
|
||||
return (new SubscriptionStatus($this, $recurring_invoice))->run();
|
||||
}
|
||||
|
||||
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
|
@ -24,6 +24,18 @@ class ComposerServiceProvider extends ServiceProvider
|
||||
public function boot()
|
||||
{
|
||||
view()->composer('portal.*', PortalComposer::class);
|
||||
|
||||
// view()->composer(
|
||||
// ['email.admin.generic', 'email.client.generic'],
|
||||
// function ($view) {
|
||||
// $view->with(
|
||||
// 'template',
|
||||
// Ninja::isHosted()
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,6 +33,7 @@ class MultiDBProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
|
||||
$this->app['events']->listen(
|
||||
JobProcessing::class,
|
||||
function ($event) {
|
||||
|
@ -141,7 +141,7 @@ class TaskRepository extends BaseRepository
|
||||
{
|
||||
|
||||
if(isset($time_log[0][0])) {
|
||||
return \Carbon\Carbon::createFromTimestamp($time_log[0][0])->addSeconds($task->company->utc_offset());
|
||||
return \Carbon\Carbon::createFromTimestamp($time_log[0][0]);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -323,13 +323,22 @@ class AutoBillInvoice extends AbstractService
|
||||
public function getGateway($amount)
|
||||
{
|
||||
//get all client gateway tokens and set the is_default one to the first record
|
||||
$gateway_tokens = $this->client
|
||||
->gateway_tokens()
|
||||
->whereHas('gateway', function ($query) {
|
||||
$query->where('is_deleted', 0)
|
||||
->where('deleted_at', null);
|
||||
})->orderBy('is_default', 'DESC')
|
||||
->get();
|
||||
$gateway_tokens = \App\Models\ClientGatewayToken::query()
|
||||
->where('client_id', $this->client->id)
|
||||
->where('is_deleted', 0)
|
||||
->whereHas('gateway', function ($query) {
|
||||
$query->where('is_deleted', 0)
|
||||
->where('deleted_at', null);
|
||||
})->orderBy('is_default', 'DESC')
|
||||
->get();
|
||||
|
||||
// $gateway_tokens = $this->client
|
||||
// ->gateway_tokens()
|
||||
// ->whereHas('gateway', function ($query) {
|
||||
// $query->where('is_deleted', 0)
|
||||
// ->where('deleted_at', null);
|
||||
// })->orderBy('is_default', 'DESC')
|
||||
// ->get();
|
||||
|
||||
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use ($amount) {
|
||||
$company_gateway = $gateway_token->gateway;
|
||||
|
128
app/Services/Subscription/ChangePlanInvoice.php
Normal file
128
app/Services/Subscription/ChangePlanInvoice.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?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\Services\Subscription;
|
||||
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Subscription;
|
||||
use App\Factory\CreditFactory;
|
||||
use App\DataMapper\InvoiceItem;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Services\AbstractService;
|
||||
use App\Repositories\CreditRepository;
|
||||
use App\Repositories\InvoiceRepository;
|
||||
use App\Repositories\SubscriptionRepository;
|
||||
|
||||
class ChangePlanInvoice extends AbstractService
|
||||
{
|
||||
protected \App\Services\Subscription\SubscriptionStatus $status;
|
||||
|
||||
public function __construct(protected RecurringInvoice $recurring_invoice, public Subscription $target, public string $hash)
|
||||
{
|
||||
}
|
||||
|
||||
public function run(): Invoice | Credit
|
||||
{
|
||||
|
||||
$this->status = $this->recurring_invoice
|
||||
->subscription
|
||||
->status($this->recurring_invoice);
|
||||
|
||||
//refund
|
||||
$refund = $this->status->getProRataRefund();
|
||||
|
||||
//newcharges
|
||||
$new_charge = $this->target->price;
|
||||
|
||||
$invoice = $this->generateInvoice($refund);
|
||||
|
||||
if($refund >= $new_charge){
|
||||
$invoice = $invoice->markPaid()->save();
|
||||
|
||||
//generate new recurring invoice at this point as we know the user has succeeded with their upgrade.
|
||||
}
|
||||
|
||||
if($refund > $new_charge)
|
||||
return $this->generateCredit($refund - $new_charge);
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
private function generateCredit(float $credit_balance): Credit
|
||||
{
|
||||
|
||||
$credit_repo = new CreditRepository();
|
||||
|
||||
$credit = CreditFactory::create($this->target->company_id, $this->target->user_id);
|
||||
$credit->status_id = Credit::STATUS_SENT;
|
||||
$credit->date = now()->addSeconds($this->recurring_invoice->client->timezone_offset())->format('Y-m-d');
|
||||
$credit->subscription_id = $this->target->id;
|
||||
|
||||
$invoice_item = new InvoiceItem();
|
||||
$invoice_item->type_id = '1';
|
||||
$invoice_item->product_key = ctrans('texts.credit');
|
||||
$invoice_item->notes = ctrans('texts.credit') . " # {$this->recurring_invoice->subscription->name} #";
|
||||
$invoice_item->quantity = 1;
|
||||
$invoice_item->cost = $credit_balance;
|
||||
|
||||
$invoice_items = [];
|
||||
$invoice_items[] = $invoice_item;
|
||||
|
||||
$data = [
|
||||
'client_id' => $this->recurring_invoice->client_id,
|
||||
'date' => now()->format('Y-m-d'),
|
||||
];
|
||||
|
||||
return $credit_repo->save($data, $credit)->service()->markSent()->fillDefaults()->save();
|
||||
|
||||
}
|
||||
|
||||
//Careful with Invoice Numbers.
|
||||
private function generateInvoice(float $refund): Invoice
|
||||
{
|
||||
|
||||
$subscription_repo = new SubscriptionRepository();
|
||||
$invoice_repo = new InvoiceRepository();
|
||||
|
||||
$invoice = InvoiceFactory::create($this->target->company_id, $this->target->user_id);
|
||||
$invoice->date = now()->format('Y-m-d');
|
||||
$invoice->subscription_id = $this->target->id;
|
||||
|
||||
$invoice_item = new InvoiceItem();
|
||||
$invoice_item->type_id = '1';
|
||||
$invoice_item->product_key = ctrans('texts.refund');
|
||||
$invoice_item->notes = ctrans('texts.refund'). " #{$this->status->refundable_invoice->number}";
|
||||
$invoice_item->quantity = 1;
|
||||
$invoice_item->cost = $refund;
|
||||
|
||||
$invoice_items = [];
|
||||
$invoice_items[] = $subscription_repo->generateLineItems($this->target);
|
||||
$invoice_items[] = $invoice_item;
|
||||
$invoice->line_items = $invoice_items;
|
||||
$invoice->is_proforma = true;
|
||||
|
||||
$data = [
|
||||
'client_id' => $this->recurring_invoice->client_id,
|
||||
'date' => now()->addSeconds($this->recurring_invoice->client->timezone_offset())->format('Y-m-d'),
|
||||
];
|
||||
|
||||
$invoice = $invoice_repo->save($data, $invoice)
|
||||
->service()
|
||||
->markSent()
|
||||
->fillDefaults()
|
||||
->save();
|
||||
|
||||
return $invoice;
|
||||
|
||||
}
|
||||
}
|
70
app/Services/Subscription/InvoiceToRecurring.php
Normal file
70
app/Services/Subscription/InvoiceToRecurring.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?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\Services\Subscription;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Services\AbstractService;
|
||||
use App\Factory\RecurringInvoiceFactory;
|
||||
use App\Repositories\SubscriptionRepository;
|
||||
|
||||
class InvoiceToRecurring extends AbstractService
|
||||
{
|
||||
|
||||
protected \App\Services\Subscription\SubscriptionStatus $status;
|
||||
|
||||
public function __construct(protected int $client_id, public Subscription $subscription, public array $bundle = [])
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function run(): RecurringInvoice
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->subscription->company->db);
|
||||
|
||||
$client = Client::withTrashed()->find($this->client_id);
|
||||
|
||||
$subscription_repo = new SubscriptionRepository();
|
||||
|
||||
$line_items = count($this->bundle) > 1 ? $subscription_repo->generateBundleLineItems($this->bundle, true, false) : $subscription_repo->generateLineItems($this->subscription, true, false);
|
||||
|
||||
$recurring_invoice = RecurringInvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
|
||||
$recurring_invoice->client_id = $this->client_id;
|
||||
$recurring_invoice->line_items = $line_items;
|
||||
$recurring_invoice->subscription_id = $this->subscription->id;
|
||||
$recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY;
|
||||
$recurring_invoice->date = now();
|
||||
$recurring_invoice->remaining_cycles = -1;
|
||||
$recurring_invoice->auto_bill = $client->getSetting('auto_bill');
|
||||
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
|
||||
$recurring_invoice->due_date_days = 'terms';
|
||||
$recurring_invoice->next_send_date = now()->format('Y-m-d');
|
||||
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
|
||||
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
|
||||
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
|
||||
|
||||
return $recurring_invoice;
|
||||
|
||||
}
|
||||
|
||||
private function setAutoBillFlag($auto_bill): bool
|
||||
{
|
||||
if ($auto_bill == 'always' || $auto_bill == 'optout') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
470
app/Services/Subscription/PaymentLinkService.php
Normal file
470
app/Services/Subscription/PaymentLinkService.php
Normal file
@ -0,0 +1,470 @@
|
||||
<?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\Services\Subscription;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\SystemLog;
|
||||
use App\Models\PaymentHash;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\ClientContact;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Models\RecurringInvoice;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use App\Services\Subscription\UpgradePrice;
|
||||
use App\Services\Subscription\ZeroCostProduct;
|
||||
use App\Repositories\RecurringInvoiceRepository;
|
||||
use App\Services\Subscription\ChangePlanInvoice;
|
||||
|
||||
class PaymentLinkService
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
public const WHITE_LABEL = 4316;
|
||||
|
||||
public function __construct(public Subscription $subscription)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* CompletePurchase
|
||||
*
|
||||
* Perform the initial purchase of a one time
|
||||
* or recurring product
|
||||
*
|
||||
* @param PaymentHash $payment_hash
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse|null
|
||||
*/
|
||||
public function completePurchase(PaymentHash $payment_hash): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse|null
|
||||
{
|
||||
|
||||
if (!property_exists($payment_hash->data, 'billing_context')) {
|
||||
throw new \Exception("Illegal entrypoint into method, payload must contain billing context");
|
||||
}
|
||||
|
||||
if ($payment_hash->data->billing_context->context == 'change_plan') {
|
||||
return $this->handlePlanChange($payment_hash);
|
||||
}
|
||||
|
||||
// if ($payment_hash->data->billing_context->context == 'whitelabel') {
|
||||
// return $this->handleWhiteLabelPurchase($payment_hash);
|
||||
// }
|
||||
|
||||
if (strlen($this->subscription->recurring_product_ids) >= 1) {
|
||||
|
||||
$bundle = isset($payment_hash->data->billing_context->bundle) ? $payment_hash->data->billing_context->bundle : [];
|
||||
$recurring_invoice = (new InvoiceToRecurring($payment_hash->payment->client_id, $this->subscription, $bundle))->run();
|
||||
|
||||
$recurring_invoice_repo = new RecurringInvoiceRepository();
|
||||
|
||||
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
|
||||
$recurring_invoice->auto_bill = $this->subscription->auto_bill;
|
||||
|
||||
/* Start the recurring service */
|
||||
$recurring_invoice->service()
|
||||
->start()
|
||||
->save();
|
||||
|
||||
//update the invoice and attach to the recurring invoice!!!!!
|
||||
$invoice = Invoice::withTrashed()->find($payment_hash->fee_invoice_id);
|
||||
$invoice->recurring_id = $recurring_invoice->id;
|
||||
$invoice->is_proforma = false;
|
||||
$invoice->save();
|
||||
|
||||
//execute any webhooks
|
||||
$context = [
|
||||
'context' => 'recurring_purchase',
|
||||
'recurring_invoice' => $recurring_invoice->hashed_id,
|
||||
'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id),
|
||||
'client' => $recurring_invoice->client->hashed_id,
|
||||
'subscription' => $this->subscription->hashed_id,
|
||||
'contact' => auth()->guard('contact')->user() ? auth()->guard('contact')->user()->hashed_id : $recurring_invoice->client->contacts()->whereNotNull('email')->first()->hashed_id,
|
||||
'account_key' => $recurring_invoice->client->custom_value2,
|
||||
];
|
||||
|
||||
if (property_exists($payment_hash->data->billing_context, 'campaign')) {
|
||||
$context['campaign'] = $payment_hash->data->billing_context->campaign;
|
||||
}
|
||||
|
||||
$response = $this->triggerWebhook($context);
|
||||
|
||||
return $this->handleRedirect('/client/recurring_invoices/' . $recurring_invoice->hashed_id);
|
||||
} else {
|
||||
$invoice = Invoice::withTrashed()->find($payment_hash->fee_invoice_id);
|
||||
|
||||
$context = [
|
||||
'context' => 'single_purchase',
|
||||
'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id),
|
||||
'client' => $invoice->client->hashed_id,
|
||||
'subscription' => $this->subscription->hashed_id,
|
||||
'account_key' => $invoice->client->custom_value2,
|
||||
];
|
||||
|
||||
//execute any webhooks
|
||||
$this->triggerWebhook($context);
|
||||
|
||||
/* 06-04-2022 */
|
||||
/* We may not be in a state where the user is present */
|
||||
if (auth()->guard('contact')) {
|
||||
return $this->handleRedirect('/client/invoices/' . $this->encodePrimaryKey($payment_hash->fee_invoice_id));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* isEligible
|
||||
* ["message" => "Success", "status_code" => 200];
|
||||
* @param ClientContact $contact
|
||||
* @return array{"message": string, "status_code": int}
|
||||
*/
|
||||
public function isEligible(ClientContact $contact): array
|
||||
{
|
||||
|
||||
$context = [
|
||||
'context' => 'is_eligible',
|
||||
'subscription' => $this->subscription->hashed_id,
|
||||
'contact' => $contact->hashed_id,
|
||||
'contact_email' => $contact->email,
|
||||
'client' => $contact->client->hashed_id,
|
||||
'account_key' => $contact->client->custom_value2,
|
||||
];
|
||||
|
||||
$response = $this->triggerWebhook($context);
|
||||
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
/* Starts the process to create a trial
|
||||
- we create a recurring invoice, which has its next_send_date as now() + trial_duration
|
||||
- we then hit the client API end point to advise the trial payload
|
||||
- we then return the user to either a predefined user endpoint, OR we return the user to the recurring invoice page.
|
||||
|
||||
* startTrial
|
||||
*
|
||||
* @param array $data{contact_id: int, client_id: int, bundle: \Illuminate\Support\Collection, coupon?: string, }
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function startTrial(array $data): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
{
|
||||
|
||||
// Redirects from here work just fine. Livewire will respect it.
|
||||
$client_contact = ClientContact::find($this->decodePrimaryKey($data['contact_id']));
|
||||
|
||||
if(is_string($data['client_id'])) {
|
||||
$data['client_id'] = $this->decodePrimaryKey($data['client_id']);
|
||||
}
|
||||
|
||||
if (!$this->subscription->trial_enabled) {
|
||||
return new \Exception("Trials are disabled for this product");
|
||||
}
|
||||
|
||||
//create recurring invoice with start date = trial_duration + 1 day
|
||||
$recurring_invoice_repo = new RecurringInvoiceRepository();
|
||||
|
||||
$bundle = [];
|
||||
|
||||
if (isset($data['bundle'])) {
|
||||
|
||||
$bundle = $data['bundle']->map(function ($bundle) {
|
||||
return (object) $bundle;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
$recurring_invoice = (new InvoiceToRecurring($client_contact->client_id, $this->subscription, $bundle))->run();
|
||||
|
||||
$recurring_invoice->next_send_date = now()->addSeconds($this->subscription->trial_duration);
|
||||
$recurring_invoice->next_send_date_client = now()->addSeconds($this->subscription->trial_duration);
|
||||
$recurring_invoice->backup = 'is_trial';
|
||||
|
||||
if (array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) {
|
||||
$recurring_invoice->discount = $this->subscription->promo_discount;
|
||||
$recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount;
|
||||
} elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) {
|
||||
$recurring_invoice->discount = $this->subscription->promo_discount;
|
||||
$recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount;
|
||||
}
|
||||
|
||||
$recurring_invoice = $recurring_invoice_repo->save($data, $recurring_invoice);
|
||||
|
||||
/* Start the recurring service */
|
||||
$recurring_invoice->service()
|
||||
->start()
|
||||
->save();
|
||||
|
||||
$context = [
|
||||
'context' => 'trial',
|
||||
'recurring_invoice' => $recurring_invoice->hashed_id,
|
||||
'client' => $recurring_invoice->client->hashed_id,
|
||||
'subscription' => $this->subscription->hashed_id,
|
||||
'account_key' => $recurring_invoice->client->custom_value2,
|
||||
];
|
||||
|
||||
//execute any webhooks
|
||||
$response = $this->triggerWebhook($context);
|
||||
|
||||
return $this->handleRedirect('/client/recurring_invoices/' . $recurring_invoice->hashed_id);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* calculateUpdatePriceV2
|
||||
*
|
||||
* Need to change the naming of the method
|
||||
*
|
||||
* @param RecurringInvoice $recurring_invoice - The Current Recurring Invoice for the subscription.
|
||||
* @param Subscription $target - The new target subscription to move to
|
||||
* @return float - the upgrade price
|
||||
*/
|
||||
public function calculateUpgradePriceV2(RecurringInvoice $recurring_invoice, Subscription $target): ?float
|
||||
{
|
||||
return (new UpgradePrice($recurring_invoice, $target))->run()->upgrade_price;
|
||||
}
|
||||
|
||||
/**
|
||||
* When changing plans, we need to generate a pro rata invoice
|
||||
*
|
||||
* @param array $data{recurring_invoice: RecurringInvoice, subscription: Subscription, target: Subscription, hash: string}
|
||||
* @return Invoice | Credit
|
||||
*/
|
||||
public function createChangePlanInvoice($data): Invoice | Credit
|
||||
{
|
||||
$recurring_invoice = $data['recurring_invoice'];
|
||||
$old_subscription = $data['subscription'];
|
||||
$target_subscription = $data['target'];
|
||||
$hash = $data['hash'];
|
||||
|
||||
return (new ChangePlanInvoice($recurring_invoice, $target_subscription, $hash))->run();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 'email' => $this->email ?? $this->contact->email,
|
||||
* 'quantity' => $this->quantity,
|
||||
* 'contact_id' => $this->contact->id,
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function handleNoPaymentRequired(array $data): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$context = (new ZeroCostProduct($this->subscription, $data))->run();
|
||||
|
||||
// Forward payload to webhook
|
||||
if (array_key_exists('context', $context)) {
|
||||
$response = $this->triggerWebhook($context);
|
||||
}
|
||||
|
||||
// Hit the redirect
|
||||
return $this->handleRedirect($context['redirect_url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Invoice $invoice
|
||||
* @return true
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function planPaid(Invoice $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->primary_contact()->first()->hashed_id : $invoice->client->contacts->first()->hashed_id,
|
||||
'invoice' => $invoice->hashed_id,
|
||||
'account_key' => $invoice->client->custom_value2,
|
||||
];
|
||||
|
||||
$response = $this->triggerWebhook($context);
|
||||
|
||||
nlog($response);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Response from payment service on
|
||||
* return from a plan change
|
||||
*
|
||||
* @param PaymentHash $payment_hash
|
||||
*/
|
||||
private function handlePlanChange(PaymentHash $payment_hash): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
{
|
||||
nlog("handle plan change");
|
||||
|
||||
$old_recurring_invoice = RecurringInvoice::query()->find($this->decodePrimaryKey($payment_hash->data->billing_context->recurring_invoice));
|
||||
|
||||
if (!$old_recurring_invoice) {
|
||||
return $this->handleRedirect('/client/recurring_invoices/');
|
||||
}
|
||||
|
||||
$old_recurring_invoice->service()->stop()->save();
|
||||
|
||||
$recurring_invoice = (new InvoiceToRecurring($old_recurring_invoice->client_id, $this->subscription, []))->run();
|
||||
|
||||
$recurring_invoice->service()
|
||||
->start()
|
||||
->save();
|
||||
|
||||
//update the invoice and attach to the recurring invoice!!!!!
|
||||
$invoice = Invoice::query()->find($payment_hash->fee_invoice_id);
|
||||
$invoice->recurring_id = $recurring_invoice->id;
|
||||
$invoice->is_proforma = false;
|
||||
$invoice->save();
|
||||
|
||||
// 29-06-2023 handle webhooks for payment intent - user may not be present.
|
||||
$context = [
|
||||
'context' => 'change_plan',
|
||||
'recurring_invoice' => $recurring_invoice->hashed_id,
|
||||
'invoice' => $this->encodePrimaryKey($payment_hash->fee_invoice_id),
|
||||
'client' => $recurring_invoice->client->hashed_id,
|
||||
'subscription' => $this->subscription->hashed_id,
|
||||
'contact' => auth()->guard('contact')->user()?->hashed_id ?? $recurring_invoice->client->contacts()->first()->hashed_id,
|
||||
'account_key' => $recurring_invoice->client->custom_value2,
|
||||
];
|
||||
|
||||
$response = $this->triggerWebhook($context);
|
||||
|
||||
nlog($response);
|
||||
|
||||
return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handles redirecting the user
|
||||
*/
|
||||
private function handleRedirect($default_redirect): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
|
||||
{
|
||||
if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url']) >= 1) {
|
||||
return method_exists(redirect(), "send") ? redirect($this->subscription->webhook_configuration['return_url'])->send() : redirect($this->subscription->webhook_configuration['return_url']);
|
||||
}
|
||||
|
||||
return method_exists(redirect(), "send") ? redirect($default_redirect)->send() : redirect($default_redirect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit a 3rd party API if defined in the subscription
|
||||
*
|
||||
* @param array $context
|
||||
* @return array
|
||||
*/
|
||||
public function triggerWebhook($context): array
|
||||
{
|
||||
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];
|
||||
}
|
||||
|
||||
$response = false;
|
||||
|
||||
$body = array_merge($context, [
|
||||
'db' => $this->subscription->company->db,
|
||||
]);
|
||||
|
||||
$response = $this->sendLoad($this->subscription, $body);
|
||||
|
||||
/* Append the response to the system logger body */
|
||||
if (is_array($response)) {
|
||||
$body = $response;
|
||||
} else {
|
||||
$body = $response->getStatusCode();
|
||||
}
|
||||
|
||||
$client = Client::query()->where('id', $this->decodePrimaryKey($body['client']))->withTrashed()->first();
|
||||
|
||||
SystemLogger::dispatch(
|
||||
$body,
|
||||
SystemLog::CATEGORY_WEBHOOK,
|
||||
SystemLog::EVENT_WEBHOOK_RESPONSE,
|
||||
SystemLog::TYPE_WEBHOOK_RESPONSE,
|
||||
$client,
|
||||
$client->company,
|
||||
);
|
||||
|
||||
nlog("ready to fire back");
|
||||
|
||||
if (is_array($body)) {
|
||||
return $response;
|
||||
} else {
|
||||
return ['message' => 'There was a problem encountered with the webhook', 'status_code' => 500];
|
||||
}
|
||||
}
|
||||
|
||||
public function sendLoad($subscription, $body)
|
||||
{
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Requested-With' => 'XMLHttpRequest',
|
||||
];
|
||||
|
||||
if (!isset($subscription->webhook_configuration['post_purchase_url']) && !isset($subscription->webhook_configuration['post_purchase_rest_method'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count($subscription->webhook_configuration['post_purchase_headers']) >= 1) {
|
||||
$headers = array_merge($headers, $subscription->webhook_configuration['post_purchase_headers']);
|
||||
}
|
||||
|
||||
$client = new \GuzzleHttp\Client(
|
||||
[
|
||||
'headers' => $headers,
|
||||
]
|
||||
);
|
||||
|
||||
$post_purchase_rest_method = (string) $subscription->webhook_configuration['post_purchase_rest_method'];
|
||||
$post_purchase_url = (string) $subscription->webhook_configuration['post_purchase_url'];
|
||||
|
||||
try {
|
||||
$response = $client->{$post_purchase_rest_method}($post_purchase_url, [
|
||||
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false,
|
||||
]);
|
||||
|
||||
return array_merge($body, json_decode($response->getBody(), true));
|
||||
} catch (ClientException $e) {
|
||||
$message = $e->getMessage();
|
||||
|
||||
$error = json_decode($e->getResponse()->getBody()->getContents());
|
||||
|
||||
if (is_null($error)) {
|
||||
nlog("empty response");
|
||||
nlog($e->getMessage());
|
||||
}
|
||||
|
||||
if ($error && property_exists($error, 'message')) {
|
||||
$message = $error->message;
|
||||
}
|
||||
|
||||
return array_merge($body, ['message' => $message, 'status_code' => 500]);
|
||||
} catch (\Exception $e) {
|
||||
return array_merge($body, ['message' => $e->getMessage(), 'status_code' => 500]);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,308 +0,0 @@
|
||||
<?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\Services\Subscription;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Subscription;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Services\AbstractService;
|
||||
|
||||
class ProRata extends AbstractService
|
||||
{
|
||||
/** @var bool $is_trial */
|
||||
private bool $is_trial = false;
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Collection<Invoice> | null $unpaid_invoices */
|
||||
private $unpaid_invoices = null;
|
||||
|
||||
/** @var bool $refundable */
|
||||
private bool $refundable = false;
|
||||
|
||||
/** @var int $pro_rata_duration */
|
||||
private int $pro_rata_duration = 0;
|
||||
|
||||
/** @var int $subscription_interval_duration */
|
||||
private int $subscription_interval_duration = 0;
|
||||
|
||||
/** @var int $pro_rata_ratio */
|
||||
private int $pro_rata_ratio = 1;
|
||||
|
||||
public function __construct(public Subscription $subscription, protected RecurringInvoice $recurring_invoice)
|
||||
{
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->setCalculations();
|
||||
}
|
||||
|
||||
private function setCalculations(): self
|
||||
{
|
||||
$this->isInTrialPeriod()
|
||||
->checkUnpaidInvoices()
|
||||
->checkRefundPeriod()
|
||||
->checkProRataDuration()
|
||||
->calculateSubscriptionIntervalDuration()
|
||||
->calculateProRataRatio();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of seconds
|
||||
* of the current interval that has been used.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function checkProRataDuration(): self
|
||||
{
|
||||
|
||||
$primary_invoice = $this->recurring_invoice
|
||||
->invoices()
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
$duration = Carbon::parse($primary_invoice->date)->startOfDay()->diffInSeconds(now());
|
||||
|
||||
$this->setProRataDuration(max(0, $duration));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function calculateProRataRatio(): self
|
||||
{
|
||||
if($this->pro_rata_duration < $this->subscription_interval_duration)
|
||||
$this->setProRataRatio($this->pro_rata_duration/$this->subscription_interval_duration);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
private function calculateSubscriptionIntervalDuration(): self
|
||||
{
|
||||
if($this->getIsTrial())
|
||||
return $this->setSubscriptionIntervalDuration(0);
|
||||
|
||||
$primary_invoice = $this->recurring_invoice
|
||||
->invoices()
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if(!$primary_invoice)
|
||||
return $this->setSubscriptionIntervalDuration(0);
|
||||
|
||||
$start = Carbon::parse($primary_invoice->date);
|
||||
$end = Carbon::parse($this->recurring_invoice->next_send_date_client);
|
||||
|
||||
$this->setSubscriptionIntervalDuration($start->diffInSeconds($end));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this subscription
|
||||
* is eligible for a refund.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function checkRefundPeriod(): self
|
||||
{
|
||||
if(!$this->subscription->refund_period || $this->subscription->refund_period === 0)
|
||||
return $this->setRefundable(false);
|
||||
|
||||
$primary_invoice = $this->recurring_invoice
|
||||
->invoices()
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if($primary_invoice &&
|
||||
$primary_invoice->status_id == Invoice::STATUS_PAID &&
|
||||
Carbon::parse($primary_invoice->date)->addSeconds($this->subscription->refund_period)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset()))
|
||||
){
|
||||
return $this->setRefundable(true);
|
||||
}
|
||||
|
||||
return $this->setRefundable(false);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers any unpaid invoices for this subscription.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function checkUnpaidInvoices(): self
|
||||
{
|
||||
$this->unpaid_invoices = $this->recurring_invoice
|
||||
->invoices()
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||
->where('balance', '>', 0)
|
||||
->get();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function setProRataRatio(int $ratio): self
|
||||
{
|
||||
$this->pro_rata_ratio = $ratio;
|
||||
|
||||
return $this;
|
||||
}
|
||||
/**
|
||||
* setSubscriptionIntervalDuration
|
||||
*
|
||||
* @param int $seconds
|
||||
* @return self
|
||||
*/
|
||||
private function setSubscriptionIntervalDuration(int $seconds): self
|
||||
{
|
||||
$this->subscription_interval_duration = $seconds;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* setProRataDuration
|
||||
*
|
||||
* @param int $seconds
|
||||
* @return self
|
||||
*/
|
||||
private function setProRataDuration(int $seconds): self
|
||||
{
|
||||
$this->pro_rata_duration = $seconds;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* setRefundable
|
||||
*
|
||||
* @param bool $refundable
|
||||
* @return self
|
||||
*/
|
||||
private function setRefundable(bool $refundable): self
|
||||
{
|
||||
$this->refundable = $refundable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this users is in their trial period
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function isInTrialPeriod(): self
|
||||
{
|
||||
|
||||
if(!$this->subscription->trial_enabled)
|
||||
return $this->setIsTrial(false);
|
||||
|
||||
$primary_invoice = $this->recurring_invoice
|
||||
->invoices()
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'asc')
|
||||
->first();
|
||||
|
||||
if($primary_invoice && Carbon::parse($primary_invoice->date)->addSeconds($this->subscription->trial_duration)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset())))
|
||||
return $this->setIsTrial(true);
|
||||
|
||||
$this->setIsTrial(false);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the is_trial flag
|
||||
*
|
||||
* @param bool $is_trial
|
||||
* @return self
|
||||
*/
|
||||
private function setIsTrial(bool $is_trial): self
|
||||
{
|
||||
$this->is_trial = $is_trial;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Getter for unpaid invoices
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection | null
|
||||
*/
|
||||
public function getUnpaidInvoices(): ?\Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return $this->unpaid_invoices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the is_trial flag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsTrial(): bool
|
||||
{
|
||||
return $this->is_trial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for refundable flag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getRefundable(): bool
|
||||
{
|
||||
return $this->refundable;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of seconds used in the current duration
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getProRataDuration(): int
|
||||
{
|
||||
return $this->pro_rata_duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* The total number of seconds in this subscription interval
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSubscriptionIntervalDuration(): int
|
||||
{
|
||||
return $this->subscription_interval_duration;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the pro rata ratio to be applied to any credit.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getProRataRatio(): int
|
||||
{
|
||||
return $this->pro_rata_ratio;
|
||||
}
|
||||
}
|
@ -763,7 +763,7 @@ class SubscriptionService
|
||||
/**
|
||||
* When changing plans, we need to generate a pro rata invoice
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $data{recurring_invoice: RecurringInvoice, subscription: Subscription, target: Subscription}
|
||||
* @return Invoice
|
||||
*/
|
||||
public function createChangePlanInvoice($data)
|
||||
@ -1087,12 +1087,12 @@ class SubscriptionService
|
||||
$recurring_invoice->line_items = $subscription_repo->generateBundleLineItems($bundle, true, false);
|
||||
$recurring_invoice->subscription_id = $this->subscription->id;
|
||||
$recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY;
|
||||
$recurring_invoice->date = now();
|
||||
$recurring_invoice->date = now()->addSeconds($client->timezone_offset());
|
||||
$recurring_invoice->remaining_cycles = -1;
|
||||
$recurring_invoice->auto_bill = $client->getSetting('auto_bill');
|
||||
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
|
||||
$recurring_invoice->due_date_days = 'terms';
|
||||
$recurring_invoice->next_send_date = now()->format('Y-m-d');
|
||||
$recurring_invoice->next_send_date = now()->addSeconds($client->timezone_offset())->format('Y-m-d');
|
||||
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
|
||||
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
|
||||
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
|
||||
@ -1352,7 +1352,7 @@ class SubscriptionService
|
||||
*
|
||||
* @return int Number of days
|
||||
*/
|
||||
private function getDaysInFrequency(): int
|
||||
public function getDaysInFrequency(): int
|
||||
{
|
||||
switch ($this->subscription->frequency_id) {
|
||||
case RecurringInvoice::FREQUENCY_DAILY:
|
||||
|
220
app/Services/Subscription/SubscriptionStatus.php
Normal file
220
app/Services/Subscription/SubscriptionStatus.php
Normal file
@ -0,0 +1,220 @@
|
||||
<?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\Services\Subscription;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Subscription;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Services\AbstractService;
|
||||
|
||||
class SubscriptionStatus extends AbstractService
|
||||
{
|
||||
public function __construct(public Subscription $subscription, protected RecurringInvoice $recurring_invoice) {}
|
||||
|
||||
/** @var bool $is_trial */
|
||||
public bool $is_trial = false;
|
||||
|
||||
/** @var bool $is_refundable */
|
||||
public bool $is_refundable = false;
|
||||
|
||||
/** @var bool $is_in_good_standing */
|
||||
public bool $is_in_good_standing = false;
|
||||
|
||||
/** @var Invoice $refundable_invoice */
|
||||
public Invoice $refundable_invoice;
|
||||
|
||||
public function run(): self
|
||||
{
|
||||
$this->checkTrial()
|
||||
->checkRefundable()
|
||||
->checkInGoodStanding();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetProRataRefund
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getProRataRefund(): float
|
||||
{
|
||||
|
||||
$subscription_interval_end_date = Carbon::parse($this->recurring_invoice->next_send_date_client);
|
||||
$subscription_interval_start_date = $subscription_interval_end_date->copy()->subDays($this->recurring_invoice->subscription->service()->getDaysInFrequency())->subDay();
|
||||
|
||||
$primary_invoice = Invoice::query()
|
||||
->where('company_id', $this->recurring_invoice->company_id)
|
||||
->where('client_id', $this->recurring_invoice->client_id)
|
||||
->where('recurring_id', $this->recurring_invoice->id)
|
||||
->whereIn('status_id', [Invoice::STATUS_PAID])
|
||||
->whereBetween('date', [$subscription_interval_start_date, $subscription_interval_end_date])
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
$this->refundable_invoice = $primary_invoice;
|
||||
|
||||
return $primary_invoice ? max(0, round(($primary_invoice->paid_to_date * $this->getProRataRatio()),2)) : 0;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* GetProRataRatio
|
||||
*
|
||||
* The ratio of days used / days in interval
|
||||
* @return float
|
||||
*/
|
||||
public function getProRataRatio():float
|
||||
{
|
||||
|
||||
$subscription_interval_end_date = Carbon::parse($this->recurring_invoice->next_send_date_client);
|
||||
$subscription_interval_start_date = $subscription_interval_end_date->copy()->subDays($this->recurring_invoice->subscription->service()->getDaysInFrequency())->subDay();
|
||||
|
||||
$primary_invoice = Invoice::query()
|
||||
->where('company_id', $this->recurring_invoice->company_id)
|
||||
->where('client_id', $this->recurring_invoice->client_id)
|
||||
->where('recurring_id', $this->recurring_invoice->id)
|
||||
->whereIn('status_id', [Invoice::STATUS_PAID])
|
||||
->whereBetween('date', [$subscription_interval_start_date, $subscription_interval_end_date])
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if(!$primary_invoice)
|
||||
return 0;
|
||||
|
||||
$subscription_start_date = Carbon::parse($primary_invoice->date)->startOfDay();
|
||||
|
||||
$days_of_subscription_used = $subscription_start_date->copy()->diffInDays(now());
|
||||
|
||||
return 1 - ($days_of_subscription_used / $this->recurring_invoice->subscription->service()->getDaysInFrequency());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckInGoodStanding
|
||||
*
|
||||
* Are there any outstanding invoices?
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function checkInGoodStanding(): self
|
||||
{
|
||||
|
||||
$this->is_in_good_standing = Invoice::query()
|
||||
->where('company_id', $this->recurring_invoice->company_id)
|
||||
->where('client_id', $this->recurring_invoice->client_id)
|
||||
->where('recurring_id', $this->recurring_invoice->id)
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
|
||||
->where('balance', '>', 0)
|
||||
->doesntExist();
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckTrial
|
||||
*
|
||||
* Check if this subscription is in its trial window.
|
||||
*
|
||||
* Trials do not have an invoice yet - only a pending recurring invoice.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function checkTrial(): self
|
||||
{
|
||||
|
||||
if(!$this->subscription->trial_enabled)
|
||||
return $this->setIsTrial(false);
|
||||
|
||||
$primary_invoice = Invoice::query()
|
||||
->where('company_id', $this->recurring_invoice->company_id)
|
||||
->where('client_id', $this->recurring_invoice->client_id)
|
||||
->where('recurring_id', $this->recurring_invoice->id)
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'asc')
|
||||
->doesntExist();
|
||||
|
||||
if($primary_invoice && Carbon::parse($this->recurring_invoice->next_send_date_client)->gte(now()->startOfDay()->addSeconds($this->recurring_invoice->client->timezone_offset()))) {
|
||||
return $this->setIsTrial(true);
|
||||
}
|
||||
|
||||
$this->setIsTrial(false);
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this subscription
|
||||
* is eligible for a refund.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function checkRefundable(): self
|
||||
{
|
||||
if(!$this->recurring_invoice->subscription->refund_period || $this->recurring_invoice->subscription->refund_period === 0)
|
||||
return $this->setRefundable(false);
|
||||
|
||||
$primary_invoice = $this->recurring_invoice
|
||||
->invoices()
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if($primary_invoice &&
|
||||
$primary_invoice->status_id == Invoice::STATUS_PAID &&
|
||||
Carbon::parse($primary_invoice->date)->addSeconds($this->recurring_invoice->subscription->refund_period)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset()))
|
||||
){
|
||||
return $this->setRefundable(true);
|
||||
}
|
||||
|
||||
return $this->setRefundable(false);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* setRefundable
|
||||
*
|
||||
* @param bool $refundable
|
||||
* @return self
|
||||
*/
|
||||
private function setRefundable(bool $refundable): self
|
||||
{
|
||||
$this->is_refundable = $refundable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the is_trial flag
|
||||
*
|
||||
* @param bool $is_trial
|
||||
* @return self
|
||||
*/
|
||||
private function setIsTrial(bool $is_trial): self
|
||||
{
|
||||
$this->is_trial = $is_trial;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
99
app/Services/Subscription/UpgradePrice.php
Normal file
99
app/Services/Subscription/UpgradePrice.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?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\Services\Subscription;
|
||||
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Services\AbstractService;
|
||||
|
||||
class UpgradePrice extends AbstractService
|
||||
{
|
||||
protected \App\Services\Subscription\SubscriptionStatus $status;
|
||||
|
||||
public float $upgrade_price = 0;
|
||||
|
||||
public float $refund = 0;
|
||||
|
||||
public float $outstanding_credit = 0;
|
||||
|
||||
public function __construct(protected RecurringInvoice $recurring_invoice, public Subscription $subscription)
|
||||
{
|
||||
}
|
||||
|
||||
public function run(): self
|
||||
{
|
||||
|
||||
$this->status = $this->recurring_invoice
|
||||
->subscription
|
||||
->status($this->recurring_invoice);
|
||||
|
||||
if($this->status->is_in_good_standing)
|
||||
$this->calculateUpgrade();
|
||||
else
|
||||
$this->upgrade_price = $this->subscription->price;
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
private function calculateUpgrade(): self
|
||||
{
|
||||
$ratio = $this->status->getProRataRatio();
|
||||
|
||||
$last_invoice = $this->recurring_invoice
|
||||
->invoices()
|
||||
->where('is_deleted', 0)
|
||||
->where('is_proforma', 0)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
$this->refund = $this->getRefundableAmount($last_invoice, $ratio);
|
||||
$this->outstanding_credit = $this->getCredits();
|
||||
|
||||
nlog("{$this->subscription->price} - {$this->refund} - {$this->outstanding_credit}");
|
||||
|
||||
$this->upgrade_price = $this->subscription->price - $this->refund - $this->outstanding_credit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function getRefundableAmount(?Invoice $invoice, float $ratio): float
|
||||
{
|
||||
if (!$invoice || !$invoice->date || $invoice->status_id != Invoice::STATUS_PAID || $ratio == 0)
|
||||
return 0;
|
||||
|
||||
return max(0, round(($invoice->paid_to_date*$ratio),2));
|
||||
}
|
||||
|
||||
private function getCredits(): float
|
||||
{
|
||||
$outstanding_credits = 0;
|
||||
|
||||
$use_credit_setting = $this->recurring_invoice->client->getSetting('use_credits_payment');
|
||||
|
||||
if($use_credit_setting){
|
||||
|
||||
$outstanding_credits = Credit::query()
|
||||
->where('client_id', $this->recurring_invoice->client_id)
|
||||
->whereIn('status_id', [Credit::STATUS_SENT,Credit::STATUS_PARTIAL])
|
||||
->where('is_deleted', 0)
|
||||
->where('balance', '>', 0)
|
||||
->sum('balance');
|
||||
|
||||
}
|
||||
|
||||
return $outstanding_credits;
|
||||
}
|
||||
|
||||
}
|
@ -108,9 +108,9 @@ trait Inviteable
|
||||
switch ($this->company->portal_mode) {
|
||||
case 'subdomain':
|
||||
|
||||
if(Ninja::isHosted())
|
||||
return 'https://router.invoiceninja.com/route/'.encrypt($domain.'/client/'.$entity_type.'/'.$this->key);
|
||||
else
|
||||
// if(Ninja::isHosted())
|
||||
// return 'https://router.invoiceninja.com/route/'.encrypt($domain.'/client/'.$entity_type.'/'.$this->key);
|
||||
// else
|
||||
return $domain.'/client/'.$entity_type.'/'.$this->key;
|
||||
break;
|
||||
case 'iframe':
|
||||
|
@ -3868,7 +3868,7 @@ $lang = array(
|
||||
'cancellation_pending' => 'Cancellation pending, we\'ll be in touch!',
|
||||
'list_of_payments' => 'List of payments',
|
||||
'payment_details' => 'Details of the payment',
|
||||
'list_of_payment_invoices' => 'List of invoices affected by the payment',
|
||||
'list_of_payment_invoices' => 'Associate invoices',
|
||||
'list_of_payment_methods' => 'List of payment methods',
|
||||
'payment_method_details' => 'Details of payment method',
|
||||
'permanently_remove_payment_method' => 'Permanently remove this payment method.',
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
228
tests/Feature/PaymentLink/PaymentLinkTest.php
Normal file
228
tests/Feature/PaymentLink/PaymentLinkTest.php
Normal file
@ -0,0 +1,228 @@
|
||||
<?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\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\Invoice;
|
||||
use Tests\MockUnitData;
|
||||
use App\Models\Subscription;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Helpers\Invoice\ProRata;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Factory\InvoiceItemFactory;
|
||||
use App\Helpers\Subscription\SubscriptionCalculator;
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
class PaymentLinkTest extends TestCase
|
||||
{
|
||||
use MockUnitData;
|
||||
|
||||
protected 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,
|
||||
]);
|
||||
|
||||
$recurring_invoice = RecurringInvoice::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' => now()->subWeeks(2),
|
||||
'next_send_date_client' => now(),
|
||||
]);
|
||||
|
||||
|
||||
$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' => now()->subWeeks(2),
|
||||
'recurring_id' => $recurring_invoice->id,
|
||||
]);
|
||||
|
||||
$recurring_invoice = $recurring_invoice->calc()->getInvoice();
|
||||
$invoice = $invoice->calc()->getInvoice();
|
||||
$this->assertEquals(10, $invoice->amount);
|
||||
$invoice->service()->markSent()->save();
|
||||
$this->assertEquals(10, $invoice->amount);
|
||||
$this->assertEquals(10, $invoice->balance);
|
||||
$invoice = $invoice->service()->markPaid()->save();
|
||||
$this->assertEquals(0, $invoice->balance);
|
||||
$this->assertEquals(10, $invoice->paid_to_date);
|
||||
|
||||
$status = $recurring_invoice
|
||||
->subscription
|
||||
->status($recurring_invoice);
|
||||
|
||||
$this->assertFalse($status->is_trial);
|
||||
$this->assertFalse($status->is_refundable);
|
||||
$this->assertTrue($status->is_in_good_standing);
|
||||
|
||||
$days = $recurring_invoice->subscription->service()->getDaysInFrequency();
|
||||
|
||||
$ratio = 1 - (14 / $days);
|
||||
|
||||
$this->assertEquals($ratio, $status->getProRataRatio());
|
||||
|
||||
$price = $target->link_service()->calculateUpgradePriceV2($recurring_invoice, $target);
|
||||
|
||||
$refund = round($invoice->paid_to_date*$ratio,2);
|
||||
|
||||
$this->assertEquals(($target->price - $refund), $price);
|
||||
|
||||
}
|
||||
|
||||
// public function testProrataDiscountRatioPercentage()
|
||||
// {
|
||||
// $subscription = Subscription::factory()->create([
|
||||
// 'company_id' => $this->company->id,
|
||||
// 'user_id' => $this->user->id,
|
||||
// 'price' => 100,
|
||||
// ]);
|
||||
|
||||
// $item = InvoiceItemFactory::create();
|
||||
// $item->quantity = 1;
|
||||
|
||||
// $item->cost = 100;
|
||||
// $item->product_key = 'xyz';
|
||||
// $item->notes = 'test';
|
||||
// $item->custom_value1 = 'x';
|
||||
// $item->custom_value2 = 'x';
|
||||
// $item->custom_value3 = 'x';
|
||||
// $item->custom_value4 = 'x';
|
||||
|
||||
// $line_items[] = $item;
|
||||
|
||||
// $invoice = Invoice::factory()->create([
|
||||
// 'line_items' => $line_items,
|
||||
// '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',
|
||||
// 'discount' => 10,
|
||||
// 'is_amount_discount' => false,
|
||||
// 'status_id' => 1,
|
||||
// ]);
|
||||
|
||||
// $invoice = $invoice->calc()->getInvoice();
|
||||
// $this->assertEquals(90, $invoice->amount);
|
||||
// $this->assertEquals(0, $invoice->balance);
|
||||
|
||||
// $invoice->service()->markSent()->save();
|
||||
|
||||
// $this->assertEquals(90, $invoice->amount);
|
||||
// $this->assertEquals(90, $invoice->balance);
|
||||
|
||||
|
||||
// $ratio = $subscription->service()->calculateDiscountRatio($invoice);
|
||||
|
||||
// $this->assertEquals(.1, $ratio);
|
||||
// }
|
||||
|
||||
// public function testProrataDiscountRatioAmount()
|
||||
// {
|
||||
// $subscription = Subscription::factory()->create([
|
||||
// 'company_id' => $this->company->id,
|
||||
// 'user_id' => $this->user->id,
|
||||
// 'price' => 100,
|
||||
// ]);
|
||||
|
||||
// $item = InvoiceItemFactory::create();
|
||||
// $item->quantity = 1;
|
||||
|
||||
// $item->cost = 100;
|
||||
// $item->product_key = 'xyz';
|
||||
// $item->notes = 'test';
|
||||
// $item->custom_value1 = 'x';
|
||||
// $item->custom_value2 = 'x';
|
||||
// $item->custom_value3 = 'x';
|
||||
// $item->custom_value4 = 'x';
|
||||
|
||||
// $line_items[] = $item;
|
||||
|
||||
// $invoice = Invoice::factory()->create([
|
||||
// 'line_items' => $line_items,
|
||||
// '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',
|
||||
// 'discount' => 20,
|
||||
// 'is_amount_discount' => true,
|
||||
// 'status_id' => 1,
|
||||
// ]);
|
||||
|
||||
// $invoice = $invoice->calc()->getInvoice();
|
||||
// $this->assertEquals(80, $invoice->amount);
|
||||
// $this->assertEquals(0, $invoice->balance);
|
||||
|
||||
// $invoice->service()->markSent()->save();
|
||||
|
||||
// $this->assertEquals(80, $invoice->amount);
|
||||
// $this->assertEquals(80, $invoice->balance);
|
||||
|
||||
|
||||
// $ratio = $subscription->service()->calculateDiscountRatio($invoice);
|
||||
|
||||
// $this->assertEquals(.2, $ratio);
|
||||
// }
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user