1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 20:22:42 +01:00
This commit is contained in:
David Bomba 2024-02-08 18:42:38 +11:00
parent 5e7a184118
commit f1b81e1587
8 changed files with 672 additions and 184 deletions

View 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;
}
}

View 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;
}
}

View File

@ -11,14 +11,27 @@
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)
@ -32,10 +45,84 @@ class PaymentLinkService
* or recurring product
*
* @param PaymentHash $payment_hash
* @return Illuminate\Routing\Redirector
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse|null
*/
public function completePurchase(PaymentHash $payment_hash): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
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;
}
@ -47,7 +134,20 @@ class PaymentLinkService
*/
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
@ -58,11 +158,68 @@ class PaymentLinkService
* startTrial
*
* @param array $data{contact_id: int, client_id: int, bundle: \Illuminate\Support\Collection, coupon?: string, }
* @return Illuminate\Routing\Redirector
* @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);
}
/**
@ -76,6 +233,238 @@ class PaymentLinkService
*/
public function calculateUpgradePriceV2(RecurringInvoice $recurring_invoice, Subscription $target): ?float
{
return (new UpgradePrice($recurring_invoice, $target))->run();
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]);
}
}
}

View File

@ -1,126 +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;
}
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
{
$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;
}
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;
}
}

View File

@ -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();

View File

@ -29,7 +29,10 @@ class SubscriptionStatus extends AbstractService
/** @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()
@ -38,15 +41,48 @@ class SubscriptionStatus extends AbstractService
return $this;
}
public function getProRataRatio():float
/**
* GetProRataRefund
*
* @return float
*/
public function getProRataRefund(): float
{
//calculate how much used.
$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()
$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)
@ -64,13 +100,15 @@ class SubscriptionStatus extends AbstractService
$days_of_subscription_used = $subscription_start_date->copy()->diffInDays(now());
return $days_of_subscription_used / $this->recurring_invoice->subscription->service()->getDaysInFrequency();
return 1 - ($days_of_subscription_used / $this->recurring_invoice->subscription->service()->getDaysInFrequency());
}
/**
* checkInGoodStanding
*
* CheckInGoodStanding
*
* Are there any outstanding invoices?
*
* @return self
*/
private function checkInGoodStanding(): self
@ -91,8 +129,12 @@ class SubscriptionStatus extends AbstractService
}
/**
* checkTrial
* 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
@ -101,14 +143,16 @@ class SubscriptionStatus extends AbstractService
if(!$this->subscription->trial_enabled)
return $this->setIsTrial(false);
$primary_invoice = $this->recurring_invoice
->invoices()
$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')
->first();
->doesntExist();
if($primary_invoice && Carbon::parse($primary_invoice->date)->addSeconds($this->subscription->trial_duration)->lte(now()->startOfDay()->addSeconds($primary_invoice->client->timezone_offset()))) {
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);
}

View File

@ -21,26 +21,33 @@ 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(): float
public function run(): self
{
$this->status = $this->recurring_invoice
->subscription
->status($this->recurring_invoice);
if($this->status->is_trial || !$this->status->is_in_good_standing)
return $this->subscription->price;
if($this->status->is_in_good_standing)
return $this->calculateUpgrade();
$this->calculateUpgrade();
else
$this->upgrade_price = $this->subscription->price;
return $this;
}
private function calculateUpgrade(): float
private function calculateUpgrade(): self
{
$ratio = $this->status->getProRataRatio();
@ -51,13 +58,14 @@ class UpgradePrice extends AbstractService
->orderBy('id', 'desc')
->first();
$refund = $this->getRefundableAmount($last_invoice, $ratio);
$outstanding_credit = $this->getCredits();
$this->refund = $this->getRefundableAmount($last_invoice, $ratio);
$this->outstanding_credit = $this->getCredits();
nlog("{$this->subscription->price} - {$refund} - {$outstanding_credit}");
nlog("{$this->subscription->price} - {$this->refund} - {$this->outstanding_credit}");
return $this->subscription->price - $refund - $outstanding_credit;
$this->upgrade_price = $this->subscription->price - $this->refund - $this->outstanding_credit;
return $this;
}
private function getRefundableAmount(?Invoice $invoice, float $ratio): float

View File

@ -104,7 +104,7 @@ class PaymentLinkTest extends TestCase
$days = $recurring_invoice->subscription->service()->getDaysInFrequency();
$ratio = (14 / $days);
$ratio = 1 - (14 / $days);
$this->assertEquals($ratio, $status->getProRataRatio());
@ -113,32 +113,7 @@ class PaymentLinkTest extends TestCase
$refund = round($invoice->paid_to_date*$ratio,2);
$this->assertEquals(($target->price - $refund), $price);
// $this->assertEquals($target->price-$refund, $upgrade_price);
// $sub_calculator = new SubscriptionCalculator($target->fresh(), $invoice->fresh());
// $this->assertFalse($sub_calculator->isPaidUp());
// $invoice = $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);
}
// public function testProrataDiscountRatioPercentage()