mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 05:02:36 +01:00
Merge remote-tracking branch 'upstream/v5-develop' into 1314-subscriptions-v3
This commit is contained in:
commit
38d9054650
@ -1 +1 @@
|
|||||||
5.8.23
|
5.8.24
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,8 +78,9 @@ class OpenApiYaml extends Command
|
|||||||
|
|
||||||
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components.yaml'));
|
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components.yaml'));
|
||||||
|
|
||||||
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components/responses.yaml'));
|
|
||||||
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components/examples.yaml'));
|
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components/examples.yaml'));
|
||||||
|
|
||||||
|
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components/responses.yaml'));
|
||||||
|
|
||||||
$directory = new DirectoryIterator($path . '/components/responses/');
|
$directory = new DirectoryIterator($path . '/components/responses/');
|
||||||
|
|
||||||
|
@ -225,22 +225,6 @@ class ClientExport extends BaseExport
|
|||||||
$entity['client.assigned_user'] = $client->assigned_user ? $client->user->present()->name() : '';
|
$entity['client.assigned_user'] = $client->assigned_user ? $client->user->present()->name() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (in_array('client.country_id', $this->input['report_keys'])) {
|
|
||||||
// $entity['client.country_id'] = $client->country ? ctrans("texts.country_{$client->country->name}") : '';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (in_array('client.shipping_country_id', $this->input['report_keys'])) {
|
|
||||||
// $entity['client.shipping_country_id'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : '';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (in_array('client.currency_id', $this->input['report_keys'])) {
|
|
||||||
// $entity['client.currency_id'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (in_array('client.industry_id', $this->input['report_keys'])) {
|
|
||||||
// $entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : '';
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (in_array('client.classification', $this->input['report_keys']) && isset($client->classification)) {
|
if (in_array('client.classification', $this->input['report_keys']) && isset($client->classification)) {
|
||||||
$entity['client.classification'] = ctrans("texts.{$client->classification}") ?? '';
|
$entity['client.classification'] = ctrans("texts.{$client->classification}") ?? '';
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,11 @@ class ClientFilters extends QueryFilters
|
|||||||
|
|
||||||
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number')
|
||||||
|
{
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->builder->orderBy($sort_col[0], $dir);
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,6 +146,11 @@ class CreditFilters extends QueryFilters
|
|||||||
->whereColumn('clients.id', 'credits.client_id'), $dir);
|
->whereColumn('clients.id', 'credits.client_id'), $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->builder->orderBy($sort_col[0], $dir);
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,6 +172,8 @@ class ExpenseFilters extends QueryFilters
|
|||||||
return $this->builder;
|
return $this->builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||||
|
|
||||||
if ($sort_col[0] == 'client_id' && in_array($sort_col[1], ['asc', 'desc'])) {
|
if ($sort_col[0] == 'client_id' && in_array($sort_col[1], ['asc', 'desc'])) {
|
||||||
return $this->builder
|
return $this->builder
|
||||||
->orderByRaw('ISNULL(client_id), client_id '. $sort_col[1])
|
->orderByRaw('ISNULL(client_id), client_id '. $sort_col[1])
|
||||||
@ -194,6 +196,10 @@ class ExpenseFilters extends QueryFilters
|
|||||||
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]);
|
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['public_notes', 'date', 'id_number', 'custom_value1', 'custom_value2', 'custom_value3', 'custom_value4'])) {
|
if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['public_notes', 'date', 'id_number', 'custom_value1', 'custom_value2', 'custom_value3', 'custom_value4'])) {
|
||||||
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
|
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
|
||||||
}
|
}
|
||||||
|
@ -318,11 +318,16 @@ class InvoiceFilters extends QueryFilters
|
|||||||
|
|
||||||
if ($sort_col[0] == 'client_id') {
|
if ($sort_col[0] == 'client_id') {
|
||||||
|
|
||||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
return $this->builder->orderBy(\App\Models\Client::select ('name')
|
||||||
->whereColumn('clients.id', 'invoices.client_id'), $dir);
|
->whereColumn('clients.id', 'invoices.client_id'), $dir);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number')
|
||||||
|
{
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->builder->orderBy($sort_col[0], $dir);
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,14 +167,18 @@ class PaymentFilters extends QueryFilters
|
|||||||
return $this->builder;
|
return $this->builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||||
|
|
||||||
if ($sort_col[0] == 'client_id') {
|
if ($sort_col[0] == 'client_id') {
|
||||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
return $this->builder->orderBy(\App\Models\Client::select('name')
|
||||||
->whereColumn('clients.id', 'payments.client_id'), $sort_col[1]);
|
->whereColumn('clients.id', 'payments.client_id'), $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function date_range(string $date_range = ''): Builder
|
public function date_range(string $date_range = ''): Builder
|
||||||
|
@ -59,21 +59,24 @@ class ProjectFilters extends QueryFilters
|
|||||||
public function sort(string $sort = ''): Builder
|
public function sort(string $sort = ''): Builder
|
||||||
{
|
{
|
||||||
$sort_col = explode('|', $sort);
|
$sort_col = explode('|', $sort);
|
||||||
|
|
||||||
if ($sort_col[0] == 'client_id') {
|
|
||||||
return $this->builder->orderBy(\App\Models\Client::select('name')
|
|
||||||
->whereColumn('clients.id', 'projects.client_id'), $sort_col[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_array($sort_col) || count($sort_col) != 2) {
|
if (!is_array($sort_col) || count($sort_col) != 2) {
|
||||||
return $this->builder;
|
return $this->builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($sort_col) && in_array($sort_col[1], ['asc','desc'])) {
|
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||||
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
|
|
||||||
|
if ($sort_col[0] == 'client_id') {
|
||||||
|
return $this->builder->orderBy(\App\Models\Client::select('name')
|
||||||
|
->whereColumn('clients.id', 'projects.client_id'), $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->builder;
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,6 +130,10 @@ class PurchaseOrderFilters extends QueryFilters
|
|||||||
->whereColumn('vendors.id', 'purchase_orders.vendor_id'), $dir);
|
->whereColumn('vendors.id', 'purchase_orders.vendor_id'), $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->builder->orderBy($sort_col[0], $dir);
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +155,10 @@ class QuoteFilters extends QueryFilters
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
if ($sort_col[0] == 'valid_until') {
|
if ($sort_col[0] == 'valid_until') {
|
||||||
$sort_col[0] = 'due_date';
|
$sort_col[0] = 'due_date';
|
||||||
}
|
}
|
||||||
|
@ -140,6 +140,8 @@ class RecurringExpenseFilters extends QueryFilters
|
|||||||
return $this->builder;
|
return $this->builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||||
|
|
||||||
if ($sort_col[0] == 'client_id' && in_array($sort_col[1], ['asc', 'desc'])) {
|
if ($sort_col[0] == 'client_id' && in_array($sort_col[1], ['asc', 'desc'])) {
|
||||||
return $this->builder
|
return $this->builder
|
||||||
->orderByRaw('ISNULL(client_id), client_id '. $sort_col[1])
|
->orderByRaw('ISNULL(client_id), client_id '. $sort_col[1])
|
||||||
@ -162,6 +164,10 @@ class RecurringExpenseFilters extends QueryFilters
|
|||||||
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]);
|
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['public_notes', 'date', 'id_number', 'custom_value1', 'custom_value2', 'custom_value3', 'custom_value4'])) {
|
if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['public_notes', 'date', 'id_number', 'custom_value1', 'custom_value2', 'custom_value3', 'custom_value4'])) {
|
||||||
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
|
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
|
||||||
}
|
}
|
||||||
|
@ -143,6 +143,10 @@ class TaskFilters extends QueryFilters
|
|||||||
->whereColumn('users.id', 'tasks.user_id'), $dir);
|
->whereColumn('users.id', 'tasks.user_id'), $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->builder->orderBy($sort_col[0], $dir);
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +71,10 @@ class VendorFilters extends QueryFilters
|
|||||||
|
|
||||||
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
|
||||||
|
|
||||||
|
if($sort_col[0] == 'number') {
|
||||||
|
return $this->builder->orderByRaw('ABS(number) ' . $dir);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->builder->orderBy($sort_col[0], $dir);
|
return $this->builder->orderBy($sort_col[0], $dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,9 +104,9 @@ class AccountTransformer implements AccountTransformerInterface
|
|||||||
return [
|
return [
|
||||||
'id' => $nordigen_account->metadata["id"],
|
'id' => $nordigen_account->metadata["id"],
|
||||||
'account_type' => "bank",
|
'account_type' => "bank",
|
||||||
'account_name' => $nordigen_account->data["iban"],
|
'account_name' => isset($nordigen_account->data["iban"]) ? $nordigen_account->data["iban"] : '',
|
||||||
'account_status' => $nordigen_account->metadata["status"],
|
'account_status' => $nordigen_account->metadata["status"],
|
||||||
'account_number' => '**** ' . substr($nordigen_account->data["iban"], -7),
|
'account_number' => isset($nordigen_account->data["iban"]) ? '**** ' . substr($nordigen_account->data["iban"], -7) : '',
|
||||||
'provider_account_id' => $nordigen_account->metadata["id"],
|
'provider_account_id' => $nordigen_account->metadata["id"],
|
||||||
'provider_id' => $nordigen_account->institution["id"],
|
'provider_id' => $nordigen_account->institution["id"],
|
||||||
'provider_name' => $nordigen_account->institution["name"],
|
'provider_name' => $nordigen_account->institution["name"],
|
||||||
|
@ -11,37 +11,46 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Events\Client\ClientWasCreated;
|
use App\Utils\Ninja;
|
||||||
use App\Events\Client\ClientWasUpdated;
|
use App\Models\Quote;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Credit;
|
||||||
|
use App\Models\Account;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\SystemLog;
|
||||||
|
use Postmark\PostmarkClient;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
use App\Factory\ClientFactory;
|
use App\Factory\ClientFactory;
|
||||||
use App\Filters\ClientFilters;
|
use App\Filters\ClientFilters;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use App\Utils\Traits\Uploadable;
|
||||||
|
use App\Utils\Traits\BulkOptions;
|
||||||
|
use App\Jobs\Client\UpdateTaxData;
|
||||||
|
use App\Utils\Traits\SavesDocuments;
|
||||||
|
use App\Repositories\ClientRepository;
|
||||||
|
use App\Events\Client\ClientWasCreated;
|
||||||
|
use App\Events\Client\ClientWasUpdated;
|
||||||
|
use App\Transformers\ClientTransformer;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Services\Template\TemplateAction;
|
||||||
|
use App\Jobs\PostMark\ProcessPostmarkWebhook;
|
||||||
use App\Http\Requests\Client\BulkClientRequest;
|
use App\Http\Requests\Client\BulkClientRequest;
|
||||||
use App\Http\Requests\Client\CreateClientRequest;
|
|
||||||
use App\Http\Requests\Client\DestroyClientRequest;
|
|
||||||
use App\Http\Requests\Client\EditClientRequest;
|
use App\Http\Requests\Client\EditClientRequest;
|
||||||
use App\Http\Requests\Client\PurgeClientRequest;
|
|
||||||
use App\Http\Requests\Client\ReactivateClientEmailRequest;
|
|
||||||
use App\Http\Requests\Client\ShowClientRequest;
|
use App\Http\Requests\Client\ShowClientRequest;
|
||||||
|
use App\Http\Requests\Client\PurgeClientRequest;
|
||||||
use App\Http\Requests\Client\StoreClientRequest;
|
use App\Http\Requests\Client\StoreClientRequest;
|
||||||
|
use App\Http\Requests\Client\CreateClientRequest;
|
||||||
use App\Http\Requests\Client\UpdateClientRequest;
|
use App\Http\Requests\Client\UpdateClientRequest;
|
||||||
use App\Http\Requests\Client\UploadClientRequest;
|
use App\Http\Requests\Client\UploadClientRequest;
|
||||||
use App\Jobs\Client\UpdateTaxData;
|
use App\Http\Requests\Client\DestroyClientRequest;
|
||||||
use App\Jobs\PostMark\ProcessPostmarkWebhook;
|
use App\Http\Requests\Client\ClientDocumentsRequest;
|
||||||
use App\Models\Account;
|
use App\Http\Requests\Client\ReactivateClientEmailRequest;
|
||||||
use App\Models\Client;
|
use App\Models\Expense;
|
||||||
use App\Models\Company;
|
use App\Models\Payment;
|
||||||
use App\Models\SystemLog;
|
use App\Models\Task;
|
||||||
use App\Repositories\ClientRepository;
|
use App\Transformers\DocumentTransformer;
|
||||||
use App\Services\Template\TemplateAction;
|
|
||||||
use App\Transformers\ClientTransformer;
|
|
||||||
use App\Utils\Ninja;
|
|
||||||
use App\Utils\Traits\BulkOptions;
|
|
||||||
use App\Utils\Traits\MakesHash;
|
|
||||||
use App\Utils\Traits\SavesDocuments;
|
|
||||||
use App\Utils\Traits\Uploadable;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Postmark\PostmarkClient;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class ClientController.
|
* Class ClientController.
|
||||||
@ -402,4 +411,24 @@ class ClientController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function documents(ClientDocumentsRequest $request, Client $client)
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->entity_type = Document::class;
|
||||||
|
|
||||||
|
$this->entity_transformer = DocumentTransformer::class;
|
||||||
|
|
||||||
|
$documents = Document::query()
|
||||||
|
->company()
|
||||||
|
->whereHasMorph('documentable', [Invoice::class, Quote::class, Credit::class, Expense::class, Payment::class, Task::class], function ($query) use($client) {
|
||||||
|
$query->where('client_id', $client->id);
|
||||||
|
})
|
||||||
|
->orWhereHasMorph('documentable', [Client::class], function ($query) use ($client){
|
||||||
|
$query->where('id', $client->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->listResponse($documents);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,10 @@ class PaymentMethodController extends Controller
|
|||||||
try {
|
try {
|
||||||
event(new MethodDeleted($payment_method, auth()->guard('contact')->user()->company, Ninja::eventVars(auth()->guard('contact')->user()->id)));
|
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->delete();
|
||||||
|
$payment_method->save();
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
nlog($e->getMessage());
|
nlog($e->getMessage());
|
||||||
|
|
||||||
|
@ -260,7 +260,7 @@ class ImportController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return $bestDelimiter;
|
return $bestDelimiter ?? ',';
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ class TokenAuth
|
|||||||
*/
|
*/
|
||||||
app('queue')->createPayloadUsing(function () use ($company_token) {
|
app('queue')->createPayloadUsing(function () use ($company_token) {
|
||||||
return ['db' => $company_token->company->db];
|
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
|
//user who once existed, but has been soft deleted
|
||||||
|
30
app/Http/Requests/Client/ClientDocumentsRequest.php
Normal file
30
app/Http/Requests/Client/ClientDocumentsRequest.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?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\Http\Requests\Client;
|
||||||
|
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
|
||||||
|
class ClientDocumentsRequest extends Request
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $user->can('view', $this->client);
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,9 @@ class PurgeClientRequest extends Request
|
|||||||
*/
|
*/
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()->isAdmin();
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $user->isAdmin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ class StoreClientRequest extends Request
|
|||||||
|
|
||||||
$rules['number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)];
|
$rules['number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)];
|
||||||
$rules['id_number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)];
|
$rules['id_number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)];
|
||||||
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,partnership,trust,charity,government,other';
|
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other';
|
||||||
|
|
||||||
return $rules;
|
return $rules;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ class UpdateClientRequest extends Request
|
|||||||
$rules['size_id'] = 'integer|nullable';
|
$rules['size_id'] = 'integer|nullable';
|
||||||
$rules['country_id'] = 'integer|nullable';
|
$rules['country_id'] = 'integer|nullable';
|
||||||
$rules['shipping_country_id'] = 'integer|nullable';
|
$rules['shipping_country_id'] = 'integer|nullable';
|
||||||
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,partnership,trust,charity,government,other';
|
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other';
|
||||||
|
|
||||||
if ($this->id_number) {
|
if ($this->id_number) {
|
||||||
$rules['id_number'] = Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id);
|
$rules['id_number'] = Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id);
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\ExpenseCategory;
|
namespace App\Http\Requests\ExpenseCategory;
|
||||||
|
|
||||||
|
use App\Models\Expense;
|
||||||
use App\Http\Requests\Request;
|
use App\Http\Requests\Request;
|
||||||
use App\Models\ExpenseCategory;
|
use App\Models\ExpenseCategory;
|
||||||
|
|
||||||
@ -23,14 +24,21 @@ class StoreExpenseCategoryRequest extends Request
|
|||||||
*/
|
*/
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()->can('create', ExpenseCategory::class);
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $user->can('create', ExpenseCategory::class) || $user->can('create', Expense::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules()
|
public function rules()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
$rules = [];
|
$rules = [];
|
||||||
|
|
||||||
$rules['name'] = 'required|unique:expense_categories,name,null,null,company_id,'.auth()->user()->companyId();
|
$rules['name'] = 'required|unique:expense_categories,name,null,null,company_id,'.$user->companyId();
|
||||||
|
|
||||||
return $this->globalRules($rules);
|
return $this->globalRules($rules);
|
||||||
}
|
}
|
||||||
|
@ -26,16 +26,24 @@ class UpdateExpenseCategoryRequest extends Request
|
|||||||
*/
|
*/
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()->can('edit', $this->expense_category);
|
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $user->can('edit', $this->expense_category);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules()
|
public function rules()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
$rules = [];
|
$rules = [];
|
||||||
|
|
||||||
if ($this->input('name')) {
|
if ($this->input('name')) {
|
||||||
// $rules['name'] = 'unique:expense_categories,name,'.$this->id.',id,company_id,'.$this->expense_category->company_id;
|
// $rules['name'] = 'unique:expense_categories,name,'.$this->id.',id,company_id,'.$this->expense_category->company_id;
|
||||||
$rules['name'] = Rule::unique('expense_categories')->where('company_id', auth()->user()->company()->id)->ignore($this->expense_category->id);
|
$rules['name'] = Rule::unique('expense_categories')->where('company_id', $user->company()->id)->ignore($this->expense_category->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rules;
|
return $rules;
|
||||||
|
@ -198,6 +198,18 @@ class Request extends FormRequest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(isset($input['public_notes']))
|
||||||
|
$input['public_notes'] = str_replace("</sc","<-", $input['public_notes']);
|
||||||
|
|
||||||
|
if(isset($input['footer']))
|
||||||
|
$input['footer'] = str_replace("</sc", "<-", $input['footer']);
|
||||||
|
|
||||||
|
if(isset($input['terms']))
|
||||||
|
$input['terms'] = str_replace("</sc", "<-", $input['terms']);
|
||||||
|
|
||||||
|
if(isset($input['private_notes']))
|
||||||
|
$input['private_notes'] = str_replace("</sc", "<-", $input['private_notes']);
|
||||||
|
|
||||||
return $input;
|
return $input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ class StoreVendorRequest extends Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id';
|
$rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id';
|
||||||
$rules['classification'] = 'bail|sometimes|nullable|in:individual,company,partnership,trust,charity,government,other';
|
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other';
|
||||||
|
|
||||||
return $rules;
|
return $rules;
|
||||||
}
|
}
|
||||||
@ -89,6 +89,10 @@ class StoreVendorRequest extends Request
|
|||||||
$input['currency_id'] = $user->company()->settings->currency_id;
|
$input['currency_id'] = $user->company()->settings->currency_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($input['name'])) {
|
||||||
|
$input['name'] = strip_tags($input['name']);
|
||||||
|
}
|
||||||
|
|
||||||
$input = $this->decodePrimaryKeys($input);
|
$input = $this->decodePrimaryKeys($input);
|
||||||
|
|
||||||
$this->replace($input);
|
$this->replace($input);
|
||||||
|
@ -74,7 +74,7 @@ class UpdateVendorRequest extends Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id';
|
$rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id';
|
||||||
$rules['classification'] = 'bail|sometimes|nullable|in:individual,company,partnership,trust,charity,government,other';
|
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other';
|
||||||
|
|
||||||
return $rules;
|
return $rules;
|
||||||
}
|
}
|
||||||
@ -92,8 +92,8 @@ class UpdateVendorRequest extends Request
|
|||||||
{
|
{
|
||||||
$input = $this->all();
|
$input = $this->all();
|
||||||
|
|
||||||
if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) {
|
if (isset($input['name'])) {
|
||||||
$input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']);
|
$input['name'] = strip_tags($input['name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('country_id', $input) && is_null($input['country_id'])) {
|
if (array_key_exists('country_id', $input) && is_null($input['country_id'])) {
|
||||||
|
@ -31,9 +31,9 @@ class TaskMap
|
|||||||
12 => 'task.duration',
|
12 => 'task.duration',
|
||||||
13 => 'task.status',
|
13 => 'task.status',
|
||||||
14 => 'task.custom_value1',
|
14 => 'task.custom_value1',
|
||||||
15 => 'task.custom_value1',
|
15 => 'task.custom_value2',
|
||||||
16 => 'task.custom_value1',
|
16 => 'task.custom_value3',
|
||||||
17 => 'task.custom_value1',
|
17 => 'task.custom_value4',
|
||||||
18 => 'task.notes',
|
18 => 'task.notes',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,8 @@ class BaseImport
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return $bestDelimiter;
|
|
||||||
|
return $bestDelimiter ?? ',';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mapCSVHeaderToKeys($csvData)
|
public function mapCSVHeaderToKeys($csvData)
|
||||||
|
@ -115,11 +115,22 @@ class TaskTransformer extends BaseTransformer
|
|||||||
$this->stubbed_timestamp = $stub_start_date->timestamp;
|
$this->stubbed_timestamp = $stub_start_date->timestamp;
|
||||||
|
|
||||||
return $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) {
|
} catch (\Exception $e) {
|
||||||
nlog($e->getMessage());
|
nlog($e->getMessage());
|
||||||
return $this->stubbed_timestamp;
|
return $this->stubbed_timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveEndDate($item)
|
private function resolveEndDate($item)
|
||||||
@ -142,9 +153,23 @@ class TaskTransformer extends BaseTransformer
|
|||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
nlog($e->getMessage());
|
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;
|
return $this->stubbed_timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTaskStatusId($item): ?int
|
private function getTaskStatusId($item): ?int
|
||||||
|
@ -111,13 +111,16 @@ $this->export_data = null;
|
|||||||
|
|
||||||
$this->export_data['users'] = $this->company->users()->withTrashed()->cursor()->map(function ($user) {
|
$this->export_data['users'] = $this->company->users()->withTrashed()->cursor()->map(function ($user) {
|
||||||
$user->account_id = $this->encodePrimaryKey($user->account_id);
|
$user->account_id = $this->encodePrimaryKey($user->account_id);
|
||||||
|
return $user;
|
||||||
})->all();
|
})->all();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$x = $this->writer->collection('users');
|
$x = $this->writer->collection('users');
|
||||||
$x->addItems($this->export_data['users']);
|
$x->addItems($this->export_data['users']);
|
||||||
$this->export_data = null;
|
$this->export_data = null;
|
||||||
|
|
||||||
|
|
||||||
$this->export_data['client_contacts'] = $this->company->client_contacts->map(function ($client_contact) {
|
$this->export_data['client_contacts'] = $this->company->client_contacts->map(function ($client_contact) {
|
||||||
$client_contact = $this->transformArrayOfKeys($client_contact, ['company_id', 'user_id', 'client_id']);
|
$client_contact = $this->transformArrayOfKeys($client_contact, ['company_id', 'user_id', 'client_id']);
|
||||||
|
|
||||||
@ -663,20 +666,9 @@ $this->writer->end();
|
|||||||
|
|
||||||
private function zipAndSend()
|
private function zipAndSend()
|
||||||
{
|
{
|
||||||
// $file_name = date('Y-m-d').'_'.str_replace([" ", "/"], ["_",""], $this->company->present()->name() . '_' . $this->company->company_key .'.zip');
|
|
||||||
|
|
||||||
$zip_path = \Illuminate\Support\Str::ascii(str_replace(".json", ".zip", $this->file_name));
|
$zip_path = \Illuminate\Support\Str::ascii(str_replace(".json", ".zip", $this->file_name));
|
||||||
// $path = 'backups';
|
|
||||||
|
|
||||||
// Storage::makeDirectory(storage_path('backups/'));
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// mkdir(storage_path('backups/'));
|
|
||||||
// } catch(\Exception $e) {
|
|
||||||
// nlog("could not create directory");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// $zip_path = storage_path('backups/'.\Illuminate\Support\Str::ascii($file_name));
|
|
||||||
$zip = new \ZipArchive();
|
$zip = new \ZipArchive();
|
||||||
|
|
||||||
if ($zip->open($zip_path, \ZipArchive::CREATE) !== true) {
|
if ($zip->open($zip_path, \ZipArchive::CREATE) !== true) {
|
||||||
@ -686,7 +678,6 @@ $this->writer->end();
|
|||||||
$zip->addFile($this->file_name);
|
$zip->addFile($this->file_name);
|
||||||
$zip->renameName($this->file_name, 'backup.json');
|
$zip->renameName($this->file_name, 'backup.json');
|
||||||
|
|
||||||
// $zip->addFromString("backup.json", json_encode($this->export_data));
|
|
||||||
$zip->close();
|
$zip->close();
|
||||||
|
|
||||||
Storage::disk(config('filesystems.default'))->put('backups/'.str_replace(".json", ".zip",$this->file_name), file_get_contents($zip_path));
|
Storage::disk(config('filesystems.default'))->put('backups/'.str_replace(".json", ".zip",$this->file_name), file_get_contents($zip_path));
|
||||||
@ -695,6 +686,10 @@ $this->writer->end();
|
|||||||
unlink($zip_path);
|
unlink($zip_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(file_exists($this->file_name)){
|
||||||
|
unlink($this->file_name);
|
||||||
|
}
|
||||||
|
|
||||||
if(Ninja::isSelfHost()) {
|
if(Ninja::isSelfHost()) {
|
||||||
$storage_path = 'backups/'.str_replace(".json", ".zip",$this->file_name);
|
$storage_path = 'backups/'.str_replace(".json", ".zip",$this->file_name);
|
||||||
} else {
|
} else {
|
||||||
@ -709,8 +704,6 @@ $this->writer->end();
|
|||||||
$t = app('translator');
|
$t = app('translator');
|
||||||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||||
|
|
||||||
// $company_reference = Company::find($this->company->id);
|
|
||||||
|
|
||||||
$nmo = new NinjaMailerObject();
|
$nmo = new NinjaMailerObject();
|
||||||
$nmo->mailable = new DownloadBackup($url, $this->company->withoutRelations());
|
$nmo->mailable = new DownloadBackup($url, $this->company->withoutRelations());
|
||||||
$nmo->to_user = $this->user;
|
$nmo->to_user = $this->user;
|
||||||
|
@ -312,7 +312,7 @@ class CompanyImport implements ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
unlink($tmp_file);
|
unlink($tmp_file);
|
||||||
unlink($this->file_location);
|
unlink(Storage::path($this->file_location));
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -34,6 +34,7 @@ class NinjaMailer extends Mailable
|
|||||||
*/
|
*/
|
||||||
public function build()
|
public function build()
|
||||||
{
|
{
|
||||||
|
|
||||||
$from_name = config('mail.from.name');
|
$from_name = config('mail.from.name');
|
||||||
|
|
||||||
if (property_exists($this->mail_obj, 'from_name')) {
|
if (property_exists($this->mail_obj, 'from_name')) {
|
||||||
|
@ -106,6 +106,7 @@ class BillingPortalPurchase extends Component
|
|||||||
public $steps = [
|
public $steps = [
|
||||||
'passed_email' => false,
|
'passed_email' => false,
|
||||||
'existing_user' => false,
|
'existing_user' => false,
|
||||||
|
'check_rff' => false,
|
||||||
'fetched_payment_methods' => false,
|
'fetched_payment_methods' => false,
|
||||||
'fetched_client' => false,
|
'fetched_client' => false,
|
||||||
'show_start_trial' => false,
|
'show_start_trial' => false,
|
||||||
@ -181,6 +182,12 @@ class BillingPortalPurchase extends Component
|
|||||||
*/
|
*/
|
||||||
public $campaign;
|
public $campaign;
|
||||||
|
|
||||||
|
public ?string $contact_first_name;
|
||||||
|
|
||||||
|
public ?string $contact_last_name;
|
||||||
|
|
||||||
|
public ?string $contact_email;
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
MultiDB::setDb($this->db);
|
MultiDB::setDb($this->db);
|
||||||
@ -316,10 +323,14 @@ class BillingPortalPurchase extends Component
|
|||||||
*/
|
*/
|
||||||
protected function getPaymentMethods(ClientContact $contact): self
|
protected function getPaymentMethods(ClientContact $contact): self
|
||||||
{
|
{
|
||||||
Auth::guard('contact')->loginUsingId($contact->id, true);
|
|
||||||
|
|
||||||
$this->contact = $contact;
|
$this->contact = $contact;
|
||||||
|
|
||||||
|
if ($contact->showRff()) {
|
||||||
|
return $this->rff();
|
||||||
|
}
|
||||||
|
|
||||||
|
Auth::guard('contact')->loginUsingId($contact->id, true);
|
||||||
|
|
||||||
if ($this->subscription->trial_enabled) {
|
if ($this->subscription->trial_enabled) {
|
||||||
$this->heading_text = ctrans('texts.plan_trial');
|
$this->heading_text = ctrans('texts.plan_trial');
|
||||||
$this->steps['show_start_trial'] = true;
|
$this->steps['show_start_trial'] = true;
|
||||||
@ -340,6 +351,33 @@ class BillingPortalPurchase extends Component
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function rff()
|
||||||
|
{
|
||||||
|
$this->contact_first_name = $this->contact->first_name;
|
||||||
|
$this->contact_last_name = $this->contact->last_name;
|
||||||
|
$this->contact_email = $this->contact->email;
|
||||||
|
|
||||||
|
$this->steps['check_rff'] = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleRff()
|
||||||
|
{
|
||||||
|
$validated = $this->validate([
|
||||||
|
'contact_first_name' => ['required'],
|
||||||
|
'contact_last_name' => ['required'],
|
||||||
|
'contact_email' => ['required', 'email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->contact->first_name = $validated['contact_first_name'];
|
||||||
|
$this->contact->last_name = $validated['contact_last_name'];
|
||||||
|
$this->contact->email = $validated['contact_email'];
|
||||||
|
$this->contact->save();
|
||||||
|
|
||||||
|
return $this->getPaymentMethods($this->contact);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middle method between selecting payment method &
|
* Middle method between selecting payment method &
|
||||||
* submitting the from to the backend.
|
* submitting the from to the backend.
|
||||||
|
@ -44,7 +44,7 @@ class SubscriptionPlanSwitch extends Component
|
|||||||
/**
|
/**
|
||||||
* @var ClientContact
|
* @var ClientContact
|
||||||
*/
|
*/
|
||||||
public $contact;
|
public ClientContact $contact;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
@ -66,6 +66,7 @@ class SubscriptionPlanSwitch extends Component
|
|||||||
'invoice' => null,
|
'invoice' => null,
|
||||||
'company_gateway_id' => null,
|
'company_gateway_id' => null,
|
||||||
'payment_method_id' => null,
|
'payment_method_id' => null,
|
||||||
|
'show_rff' => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,6 +76,12 @@ class SubscriptionPlanSwitch extends Component
|
|||||||
|
|
||||||
public $company;
|
public $company;
|
||||||
|
|
||||||
|
public ?string $first_name;
|
||||||
|
|
||||||
|
public ?string $last_name;
|
||||||
|
|
||||||
|
public ?string $email;
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
MultiDB::setDb($this->company->db);
|
MultiDB::setDb($this->company->db);
|
||||||
@ -84,6 +91,31 @@ class SubscriptionPlanSwitch extends Component
|
|||||||
$this->methods = $this->contact->client->service()->getPaymentMethods($this->amount);
|
$this->methods = $this->contact->client->service()->getPaymentMethods($this->amount);
|
||||||
|
|
||||||
$this->hash = Str::uuid()->toString();
|
$this->hash = Str::uuid()->toString();
|
||||||
|
|
||||||
|
$this->state['show_rff'] = auth()->guard('contact')->user()->showRff();
|
||||||
|
|
||||||
|
$this->first_name = $this->contact->first_name;
|
||||||
|
|
||||||
|
$this->last_name = $this->contact->last_name;
|
||||||
|
|
||||||
|
$this->email = $this->contact->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleRff()
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'first_name' => ['required'],
|
||||||
|
'last_name' => ['required'],
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->contact->update([
|
||||||
|
'first_name' => $this->first_name,
|
||||||
|
'last_name' => $this->last_name,
|
||||||
|
'email' => $this->email,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->state['show_rff'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleBeforePaymentEvents(): void
|
public function handleBeforePaymentEvents(): void
|
||||||
|
@ -294,6 +294,11 @@ class Account extends BaseModel
|
|||||||
return Ninja::isNinja() ? ($this->isPaidHostedClient() && !$this->isTrial()) : $this->hasFeature(self::FEATURE_WHITE_LABEL);
|
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
|
public function isPaidHostedClient(): bool
|
||||||
{
|
{
|
||||||
if (!Ninja::isNinja()) {
|
if (!Ninja::isNinja()) {
|
||||||
|
@ -77,6 +77,7 @@ use Illuminate\Support\Carbon;
|
|||||||
* @property-read mixed $hashed_id
|
* @property-read mixed $hashed_id
|
||||||
* @property-read \App\Models\User $user
|
* @property-read \App\Models\User $user
|
||||||
* @property-read \App\Models\Vendor|null $vendor
|
* @property-read \App\Models\Vendor|null $vendor
|
||||||
|
* @property-read \App\Models\ExpenseCategory|null $category
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel company()
|
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel company()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel exclude($columns)
|
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel exclude($columns)
|
||||||
* @method static \Database\Factories\RecurringExpenseFactory factory($count = null, $state = [])
|
* @method static \Database\Factories\RecurringExpenseFactory factory($count = null, $state = [])
|
||||||
@ -140,17 +141,6 @@ use Illuminate\Support\Carbon;
|
|||||||
* @method static \Illuminate\Database\Eloquent\Builder|RecurringExpense withTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|RecurringExpense withTrashed()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|RecurringExpense withoutTrashed()
|
* @method static \Illuminate\Database\Eloquent\Builder|RecurringExpense withoutTrashed()
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
|
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class RecurringExpense extends BaseModel
|
class RecurringExpense extends BaseModel
|
||||||
@ -247,6 +237,12 @@ class RecurringExpense extends BaseModel
|
|||||||
return $this->belongsTo(Client::class);
|
return $this->belongsTo(Client::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ExpenseCategory::class)->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service entry points.
|
* Service entry points.
|
||||||
*/
|
*/
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Services\Subscription\PaymentLinkService;
|
||||||
use App\Services\Subscription\SubscriptionService;
|
use App\Services\Subscription\SubscriptionService;
|
||||||
|
use App\Services\Subscription\SubscriptionStatus;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
@ -121,6 +123,8 @@ class Subscription extends BaseModel
|
|||||||
'updated_at' => 'timestamp',
|
'updated_at' => 'timestamp',
|
||||||
'created_at' => 'timestamp',
|
'created_at' => 'timestamp',
|
||||||
'deleted_at' => 'timestamp',
|
'deleted_at' => 'timestamp',
|
||||||
|
'trial_enabled' => 'boolean',
|
||||||
|
'allow_plan_changes' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $with = [
|
protected $with = [
|
||||||
@ -132,6 +136,16 @@ class Subscription extends BaseModel
|
|||||||
return new SubscriptionService($this);
|
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
|
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Company::class);
|
return $this->belongsTo(Company::class);
|
||||||
|
@ -24,6 +24,18 @@ class ComposerServiceProvider extends ServiceProvider
|
|||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
view()->composer('portal.*', PortalComposer::class);
|
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()
|
public function register()
|
||||||
{
|
{
|
||||||
|
|
||||||
$this->app['events']->listen(
|
$this->app['events']->listen(
|
||||||
JobProcessing::class,
|
JobProcessing::class,
|
||||||
function ($event) {
|
function ($event) {
|
||||||
|
@ -141,7 +141,7 @@ class TaskRepository extends BaseRepository
|
|||||||
{
|
{
|
||||||
|
|
||||||
if(isset($time_log[0][0])) {
|
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;
|
return null;
|
||||||
|
@ -323,13 +323,22 @@ class AutoBillInvoice extends AbstractService
|
|||||||
public function getGateway($amount)
|
public function getGateway($amount)
|
||||||
{
|
{
|
||||||
//get all client gateway tokens and set the is_default one to the first record
|
//get all client gateway tokens and set the is_default one to the first record
|
||||||
$gateway_tokens = $this->client
|
$gateway_tokens = \App\Models\ClientGatewayToken::query()
|
||||||
->gateway_tokens()
|
->where('client_id', $this->client->id)
|
||||||
->whereHas('gateway', function ($query) {
|
->where('is_deleted', 0)
|
||||||
$query->where('is_deleted', 0)
|
->whereHas('gateway', function ($query) {
|
||||||
->where('deleted_at', null);
|
$query->where('is_deleted', 0)
|
||||||
})->orderBy('is_default', 'DESC')
|
->where('deleted_at', null);
|
||||||
->get();
|
})->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) {
|
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use ($amount) {
|
||||||
$company_gateway = $gateway_token->gateway;
|
$company_gateway = $gateway_token->gateway;
|
||||||
|
@ -1239,7 +1239,7 @@ class PdfBuilder
|
|||||||
public function productTable(): array
|
public function productTable(): array
|
||||||
{
|
{
|
||||||
$product_items = collect($this->service->config->entity->line_items)->filter(function ($item) {
|
$product_items = collect($this->service->config->entity->line_items)->filter(function ($item) {
|
||||||
return $item->type_id == 1 || $item->type_id == 6 || $item->type_id == 5;
|
return $item->type_id == 1 || $item->type_id == 6 || $item->type_id == 5 || $item->type_id == 4;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (count($product_items) == 0) {
|
if (count($product_items) == 0) {
|
||||||
|
@ -441,7 +441,7 @@ class Design extends BaseDesign
|
|||||||
public function productTable(): array
|
public function productTable(): array
|
||||||
{
|
{
|
||||||
$product_items = collect($this->entity->line_items)->filter(function ($item) {
|
$product_items = collect($this->entity->line_items)->filter(function ($item) {
|
||||||
return $item->type_id == 1 || $item->type_id == 6 || $item->type_id == 5;
|
return $item->type_id == 1 || $item->type_id == 6 || $item->type_id == 5 || $item->type_id == 4;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (count($product_items) == 0) {
|
if (count($product_items) == 0) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
469
app/Services/Subscription/PaymentLinkService.php
Normal file
469
app/Services/Subscription/PaymentLinkService.php
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
<?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
|
||||||
|
*/
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -763,7 +763,7 @@ class SubscriptionService
|
|||||||
/**
|
/**
|
||||||
* When changing plans, we need to generate a pro rata invoice
|
* 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
|
* @return Invoice
|
||||||
*/
|
*/
|
||||||
public function createChangePlanInvoice($data)
|
public function createChangePlanInvoice($data)
|
||||||
@ -1087,12 +1087,12 @@ class SubscriptionService
|
|||||||
$recurring_invoice->line_items = $subscription_repo->generateBundleLineItems($bundle, true, false);
|
$recurring_invoice->line_items = $subscription_repo->generateBundleLineItems($bundle, true, false);
|
||||||
$recurring_invoice->subscription_id = $this->subscription->id;
|
$recurring_invoice->subscription_id = $this->subscription->id;
|
||||||
$recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY;
|
$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->remaining_cycles = -1;
|
||||||
$recurring_invoice->auto_bill = $client->getSetting('auto_bill');
|
$recurring_invoice->auto_bill = $client->getSetting('auto_bill');
|
||||||
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
|
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
|
||||||
$recurring_invoice->due_date_days = 'terms';
|
$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_client = now()->format('Y-m-d');
|
||||||
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
|
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
|
||||||
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
|
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
|
||||||
@ -1352,7 +1352,7 @@ class SubscriptionService
|
|||||||
*
|
*
|
||||||
* @return int Number of days
|
* @return int Number of days
|
||||||
*/
|
*/
|
||||||
private function getDaysInFrequency(): int
|
public function getDaysInFrequency(): int
|
||||||
{
|
{
|
||||||
switch ($this->subscription->frequency_id) {
|
switch ($this->subscription->frequency_id) {
|
||||||
case RecurringInvoice::FREQUENCY_DAILY:
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -74,6 +74,12 @@ trait CleanLineItems
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(isset($item['notes']))
|
||||||
|
$item['notes'] = str_replace("</", "<-", $item['notes']);
|
||||||
|
|
||||||
|
if(isset($item['product_key']))
|
||||||
|
$item['product_key'] = str_replace("</", "<-", $item['product_key']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('id', $item) || array_key_exists('_id', $item)) {
|
if (array_key_exists('id', $item) || array_key_exists('_id', $item)) {
|
||||||
|
@ -108,9 +108,9 @@ trait Inviteable
|
|||||||
switch ($this->company->portal_mode) {
|
switch ($this->company->portal_mode) {
|
||||||
case 'subdomain':
|
case 'subdomain':
|
||||||
|
|
||||||
if(Ninja::isHosted())
|
// if(Ninja::isHosted())
|
||||||
return 'https://router.invoiceninja.com/route/'.encrypt($domain.'/client/'.$entity_type.'/'.$this->key);
|
// return 'https://router.invoiceninja.com/route/'.encrypt($domain.'/client/'.$entity_type.'/'.$this->key);
|
||||||
else
|
// else
|
||||||
return $domain.'/client/'.$entity_type.'/'.$this->key;
|
return $domain.'/client/'.$entity_type.'/'.$this->key;
|
||||||
break;
|
break;
|
||||||
case 'iframe':
|
case 'iframe':
|
||||||
|
@ -17,8 +17,8 @@ return [
|
|||||||
'require_https' => env('REQUIRE_HTTPS', true),
|
'require_https' => env('REQUIRE_HTTPS', true),
|
||||||
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
||||||
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
||||||
'app_version' => env('APP_VERSION', '5.8.23'),
|
'app_version' => env('APP_VERSION', '5.8.24'),
|
||||||
'app_tag' => env('APP_TAG', '5.8.23'),
|
'app_tag' => env('APP_TAG', '5.8.24'),
|
||||||
'minimum_client_version' => '5.0.16',
|
'minimum_client_version' => '5.0.16',
|
||||||
'terms_version' => '1.0.1',
|
'terms_version' => '1.0.1',
|
||||||
'api_secret' => env('API_SECRET', false),
|
'api_secret' => env('API_SECRET', false),
|
||||||
|
@ -3868,7 +3868,7 @@ $lang = array(
|
|||||||
'cancellation_pending' => 'Cancellation pending, we\'ll be in touch!',
|
'cancellation_pending' => 'Cancellation pending, we\'ll be in touch!',
|
||||||
'list_of_payments' => 'List of payments',
|
'list_of_payments' => 'List of payments',
|
||||||
'payment_details' => 'Details of the payment',
|
'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',
|
'list_of_payment_methods' => 'List of payment methods',
|
||||||
'payment_method_details' => 'Details of payment method',
|
'payment_method_details' => 'Details of payment method',
|
||||||
'permanently_remove_payment_method' => 'Permanently remove this payment method.',
|
'permanently_remove_payment_method' => 'Permanently remove this payment method.',
|
||||||
@ -5120,7 +5120,7 @@ $lang = array(
|
|||||||
'set_private' => 'Set private',
|
'set_private' => 'Set private',
|
||||||
'individual' => 'Individual',
|
'individual' => 'Individual',
|
||||||
'business' => 'Business',
|
'business' => 'Business',
|
||||||
'partnership' => 'partnership',
|
'partnership' => 'Partnership',
|
||||||
'trust' => 'Trust',
|
'trust' => 'Trust',
|
||||||
'charity' => 'Charity',
|
'charity' => 'Charity',
|
||||||
'government' => 'Government',
|
'government' => 'Government',
|
||||||
|
14679
openapi/api-docs.yaml
14679
openapi/api-docs.yaml
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@ components:
|
|||||||
description: 'The total number of requests in a given time window.'
|
description: 'The total number of requests in a given time window.'
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
components:
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
ApiKeyAuth:
|
ApiKeyAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
|
@ -1,72 +1,75 @@
|
|||||||
examples:
|
#examples:
|
||||||
Client:
|
# Client:
|
||||||
- id: Opnel5aKBz
|
# $ref: '#/components/schemas/Client'
|
||||||
user_id: Ua6Rw4pVbS
|
|
||||||
assigned_user_id: Ua6Rw4pVbS
|
# Client:
|
||||||
company_id: Co7Vn3yLmW
|
# id: Opnel5aKBz
|
||||||
name: "Jim's Housekeeping"
|
# user_id: Ua6Rw4pVbS
|
||||||
website: https://www.jims-housekeeping.com
|
# assigned_user_id: Ua6Rw4pVbS
|
||||||
private_notes: Client prefers email communication over phone calls
|
# company_id: Co7Vn3yLmW
|
||||||
client_hash: asdfkjhk342hjhbfdvmnfb1
|
# name: "Jim's Housekeeping"
|
||||||
industry_id: 5
|
# website: https://www.jims-housekeeping.com
|
||||||
size_id: 2
|
# private_notes: Client prefers email communication over phone calls
|
||||||
address1: 123 Main St
|
# client_hash: asdfkjhk342hjhbfdvmnfb1
|
||||||
address2: Apt 4B
|
# industry_id: 5
|
||||||
city: Beverly Hills
|
# size_id: 2
|
||||||
state: California
|
# address1: 123 Main St
|
||||||
postal_code: 90210
|
# address2: Apt 4B
|
||||||
phone: 555-3434-3434
|
# city: Beverly Hills
|
||||||
country_id: 1
|
# state: California
|
||||||
custom_value1: Email
|
# postal_code: 90210
|
||||||
custom_value2: John Doe
|
# phone: 555-3434-3434
|
||||||
custom_value3: Yes
|
# country_id: 1
|
||||||
custom_value4: $50,000
|
# custom_value1: Email
|
||||||
vat_number: VAT123456
|
# custom_value2: John Doe
|
||||||
id_number: ID123456
|
# custom_value3: Yes
|
||||||
number: CL-0001
|
# custom_value4: $50,000
|
||||||
shipping_address1: 5 Wallaby Way
|
# vat_number: VAT123456
|
||||||
shipping_address2: Suite 5
|
# id_number: ID123456
|
||||||
shipping_city: Perth
|
# number: CL-0001
|
||||||
shipping_state: Western Australia
|
# shipping_address1: 5 Wallaby Way
|
||||||
shipping_postal_code: 6110
|
# shipping_address2: Suite 5
|
||||||
shipping_country_id: 4
|
# shipping_city: Perth
|
||||||
is_deleted: false
|
# shipping_state: Western Australia
|
||||||
balance: 500.00
|
# shipping_postal_code: 6110
|
||||||
paid_to_date: 2000.00
|
# shipping_country_id: 4
|
||||||
credit_balance: 100.00
|
# is_deleted: false
|
||||||
last_login: 1628686031
|
# balance: 500.00
|
||||||
created_at: 1617629031
|
# paid_to_date: 2000.00
|
||||||
updated_at: 1628445631
|
# credit_balance: 100.00
|
||||||
group_settings_id: Opnel5aKBz
|
# last_login: 1628686031
|
||||||
routing_id: Opnel5aKBz3489-dfkiu-2239-sdsd
|
# created_at: 1617629031
|
||||||
is_tax_exempt: false
|
# updated_at: 1628445631
|
||||||
has_valid_vat_number: false
|
# group_settings_id: Opnel5aKBz
|
||||||
payment_balance: 100
|
# routing_id: Opnel5aKBz3489-dfkiu-2239-sdsd
|
||||||
contacts:
|
# is_tax_exempt: false
|
||||||
- id: Opnel5aKBz
|
# has_valid_vat_number: false
|
||||||
first_name: John
|
# payment_balance: 100
|
||||||
last_name: Doe
|
# contacts:
|
||||||
email: jim@gmail.com
|
# id: Opnel5aKBz
|
||||||
phone: 555-3434-3434
|
# first_name: John
|
||||||
send_invoice: true
|
# last_name: Doe
|
||||||
custom_value1: Email
|
# email: jim@gmail.com
|
||||||
custom_value2: John Doe
|
# phone: 555-3434-3434
|
||||||
custom_value3: Yes
|
# send_invoice: true
|
||||||
custom_value4: $50,000
|
# custom_value1: Email
|
||||||
is_primary: true
|
# custom_value2: John Doe
|
||||||
created_at: 1617629031
|
# custom_value3: Yes
|
||||||
updated_at: 1628445631
|
# custom_value4: $50,000
|
||||||
deleted_at: 1628445631
|
# is_primary: true
|
||||||
Meta:
|
# created_at: 1617629031
|
||||||
value:
|
# updated_at: 1628445631
|
||||||
pagination:
|
# deleted_at: 1628445631
|
||||||
total: 1
|
# Meta:
|
||||||
count: 1
|
# value:
|
||||||
per_page: 20
|
# pagination:
|
||||||
current_page: 1
|
# total: 1
|
||||||
total_pages: 1
|
# count: 1
|
||||||
links:
|
# per_page: 20
|
||||||
- first: https://invoicing.co/api/v1/invoices?page=1
|
# current_page: 1
|
||||||
- last: https://invoicing.co/api/v1/invoices?page=1
|
# total_pages: 1
|
||||||
- prev: null
|
# links:
|
||||||
- next: null
|
# - first: https://invoicing.co/api/v1/invoices?page=1
|
||||||
|
# - last: https://invoicing.co/api/v1/invoices?page=1
|
||||||
|
# - prev: null
|
||||||
|
# - next: null
|
@ -139,7 +139,7 @@
|
|||||||
description: The number of records to return for each request, default is 20
|
description: The number of records to return for each request, default is 20
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: int
|
type: integer
|
||||||
example: 20
|
example: 20
|
||||||
page_meta:
|
page_meta:
|
||||||
name: page
|
name: page
|
||||||
@ -147,7 +147,7 @@
|
|||||||
description: The page number to return for this request (when performing pagination), default is 1
|
description: The page number to return for this request (when performing pagination), default is 1
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: int
|
type: integer
|
||||||
example: 1
|
example: 1
|
||||||
include:
|
include:
|
||||||
name: include
|
name: include
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
Filters the entity list by entities that have been deleted.
|
Filters the entity list by entities that have been deleted.
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: booleans
|
type: boolean
|
||||||
example: ?is_deleted=true
|
example: ?is_deleted=true
|
||||||
vendor_id:
|
vendor_id:
|
||||||
name: vendor_id
|
name: vendor_id
|
||||||
|
@ -3,4 +3,4 @@
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#components/schemas/InvalidInputError'
|
$ref: '#/components/schemas/InvalidInputError'
|
@ -3,4 +3,4 @@
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#components/schemas/AuthenticationError'
|
$ref: '#/components/schemas/AuthenticationError'
|
@ -3,4 +3,4 @@
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#components/schemas/AuthorizationError'
|
$ref: '#/components/schemas/AuthorizationError'
|
@ -143,7 +143,7 @@
|
|||||||
type: boolean
|
type: boolean
|
||||||
example: true
|
example: true
|
||||||
default_auto_bill:
|
default_auto_bill:
|
||||||
type: enum
|
type: string
|
||||||
example: 'always'
|
example: 'always'
|
||||||
description: |
|
description: |
|
||||||
A flag determining whether to auto-bill clients by default
|
A flag determining whether to auto-bill clients by default
|
||||||
|
@ -7,11 +7,9 @@
|
|||||||
settings:
|
settings:
|
||||||
description: 'Settings that are used for the frontend applications to store user preferences / metadata'
|
description: 'Settings that are used for the frontend applications to store user preferences / metadata'
|
||||||
type: object
|
type: object
|
||||||
example: 'json object'
|
|
||||||
react_settings:
|
react_settings:
|
||||||
description: 'Dedicated settings object for the react web application'
|
description: 'Dedicated settings object for the react web application'
|
||||||
type: object'
|
type: object
|
||||||
example: 'json object'
|
|
||||||
is_owner:
|
is_owner:
|
||||||
description: 'Determines whether the user owns this company'
|
description: 'Determines whether the user owns this company'
|
||||||
type: boolean
|
type: boolean
|
||||||
|
@ -101,7 +101,7 @@
|
|||||||
type: number
|
type: number
|
||||||
format: float
|
format: float
|
||||||
example: '10.00'
|
example: '10.00'
|
||||||
readOnly:
|
readOnly: true
|
||||||
line_items:
|
line_items:
|
||||||
type: array
|
type: array
|
||||||
description: 'An array of objects which define the line items of the invoice'
|
description: 'An array of objects which define the line items of the invoice'
|
||||||
|
@ -31,6 +31,6 @@
|
|||||||
example: 1
|
example: 1
|
||||||
readOnly: true
|
readOnly: true
|
||||||
links:
|
links:
|
||||||
type: array
|
type: object
|
||||||
description: 'The pagination links'
|
description: 'The pagination links'
|
||||||
readOnly: true
|
readOnly: true
|
@ -10,99 +10,99 @@
|
|||||||
type: string
|
type: string
|
||||||
description: 'The hashed ID of the user assigned to this product.'
|
description: 'The hashed ID of the user assigned to this product.'
|
||||||
example: pR0j3
|
example: pR0j3
|
||||||
required: false
|
|
||||||
project_id:
|
project_id:
|
||||||
type: string
|
type: string
|
||||||
description: 'The hashed ID of the project that this product is associated with.'
|
description: 'The hashed ID of the project that this product is associated with.'
|
||||||
example: pR0j3
|
example: pR0j3
|
||||||
required: false
|
|
||||||
vendor_id:
|
vendor_id:
|
||||||
type: string
|
type: string
|
||||||
description: 'The hashed ID of the vendor that this product is associated with.'
|
description: 'The hashed ID of the vendor that this product is associated with.'
|
||||||
example: pR0j3
|
example: pR0j3
|
||||||
required: false
|
|
||||||
custom_value1:
|
custom_value1:
|
||||||
type: string
|
type: string
|
||||||
description: 'Custom value field 1.'
|
description: 'Custom value field 1.'
|
||||||
example: 'Custom value 1'
|
example: 'Custom value 1'
|
||||||
required: false
|
|
||||||
custom_value2:
|
custom_value2:
|
||||||
type: string
|
type: string
|
||||||
description: 'Custom value field 2.'
|
description: 'Custom value field 2.'
|
||||||
example: 'Custom value 2'
|
example: 'Custom value 2'
|
||||||
required: false
|
|
||||||
custom_value3:
|
custom_value3:
|
||||||
type: string
|
type: string
|
||||||
description: 'Custom value field 3.'
|
description: 'Custom value field 3.'
|
||||||
example: 'Custom value 3'
|
example: 'Custom value 3'
|
||||||
required: false
|
|
||||||
custom_value4:
|
custom_value4:
|
||||||
type: string
|
type: string
|
||||||
description: 'Custom value field 4.'
|
description: 'Custom value field 4.'
|
||||||
example: 'Custom value 4'
|
example: 'Custom value 4'
|
||||||
required: false
|
|
||||||
product_key:
|
product_key:
|
||||||
type: string
|
type: string
|
||||||
description: 'The product key.'
|
description: 'The product key.'
|
||||||
example: '1234'
|
example: '1234'
|
||||||
required: false
|
|
||||||
notes:
|
notes:
|
||||||
type: string
|
type: string
|
||||||
description: 'Notes about the product.'
|
description: 'Notes about the product.'
|
||||||
example: 'These are some notes about the product.'
|
example: 'These are some notes about the product.'
|
||||||
required: false
|
|
||||||
cost:
|
cost:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
description: 'The cost of the product.'
|
description: 'The cost of the product.'
|
||||||
example: 10.0
|
example: 10.0
|
||||||
required: false
|
|
||||||
price:
|
price:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
description: 'The price of the product.'
|
description: 'The price of the product.'
|
||||||
example: 20.0
|
example: 20.0
|
||||||
required: false
|
|
||||||
quantity:
|
quantity:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
description: 'The quantity of the product.'
|
description: 'The quantity of the product.'
|
||||||
example: 5.0
|
example: 5.0
|
||||||
required: false
|
|
||||||
default: 1
|
default: 1
|
||||||
tax_name1:
|
tax_name1:
|
||||||
type: string
|
type: string
|
||||||
description: 'The name of tax 1.'
|
description: 'The name of tax 1.'
|
||||||
example: 'Tax 1'
|
example: 'Tax 1'
|
||||||
required: false
|
|
||||||
tax_rate1:
|
tax_rate1:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
description: 'The rate of tax 1.'
|
description: 'The rate of tax 1.'
|
||||||
example: 10.0
|
example: 10.0
|
||||||
required: false
|
|
||||||
tax_name2:
|
tax_name2:
|
||||||
type: string
|
type: string
|
||||||
description: 'The name of tax 2.'
|
description: 'The name of tax 2.'
|
||||||
example: 'Tax 2'
|
example: 'Tax 2'
|
||||||
required: false
|
|
||||||
tax_rate2:
|
tax_rate2:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
description: 'The rate of tax 2.'
|
description: 'The rate of tax 2.'
|
||||||
example: 5.0
|
example: 5.0
|
||||||
required: false
|
|
||||||
tax_name3:
|
tax_name3:
|
||||||
type: string
|
type: string
|
||||||
description: 'The name of tax 3.'
|
description: 'The name of tax 3.'
|
||||||
example: 'Tax 3'
|
example: 'Tax 3'
|
||||||
required: false
|
|
||||||
tax_rate3:
|
tax_rate3:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
description: 'The rate of tax 3.'
|
description: 'The rate of tax 3.'
|
||||||
example: 0.0
|
example: 0.0
|
||||||
required: false
|
|
||||||
in_stock_quantity:
|
in_stock_quantity:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
@ -114,32 +114,32 @@
|
|||||||
The query parameter ?update_in_stock_quantity=true **MUST** be passed if you wish to update this value manually.
|
The query parameter ?update_in_stock_quantity=true **MUST** be passed if you wish to update this value manually.
|
||||||
|
|
||||||
default: 0
|
default: 0
|
||||||
required: false
|
|
||||||
stock_notification:
|
stock_notification:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Indicates whether stock notifications are enabled for this product
|
description: Indicates whether stock notifications are enabled for this product
|
||||||
default: true
|
default: true
|
||||||
required: false
|
|
||||||
stock_notification_threshold:
|
stock_notification_threshold:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
description: The minimum quantity threshold for which stock notifications will be triggered
|
description: The minimum quantity threshold for which stock notifications will be triggered
|
||||||
default: 0
|
default: 0
|
||||||
required: false
|
|
||||||
max_quantity:
|
max_quantity:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
description: The maximum quantity that can be ordered for this product
|
description: The maximum quantity that can be ordered for this product
|
||||||
required: false
|
|
||||||
product_image:
|
product_image:
|
||||||
type: string
|
type: string
|
||||||
description: The URL of the product image
|
description: The URL of the product image
|
||||||
format: uri-reference
|
format: uri-reference
|
||||||
required: false
|
|
||||||
tax_id:
|
tax_id:
|
||||||
type: string
|
type: string
|
||||||
default: '1'
|
default: '1'
|
||||||
required: false
|
|
||||||
description: |
|
description: |
|
||||||
The tax category id for this product.'
|
The tax category id for this product.'
|
||||||
|
|
||||||
|
@ -36,7 +36,6 @@ tags:
|
|||||||
description: |
|
description: |
|
||||||
Endpoint definitions for interacting with vendors.
|
Endpoint definitions for interacting with vendors.
|
||||||
- name: Purchase Orders
|
- name: Purchase Orders
|
||||||
summary: Purchase Orders
|
|
||||||
description: |
|
description: |
|
||||||
Endpoint definitions for interacting with purchase orders.
|
Endpoint definitions for interacting with purchase orders.
|
||||||
- name: expenses
|
- name: expenses
|
||||||
|
@ -2,8 +2,8 @@ paths:
|
|||||||
/api/v1/activities:
|
/api/v1/activities:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- actvities
|
- activities
|
||||||
summary: "Returns a list of actvities"
|
summary: "Returns a list of activities"
|
||||||
description: "Lists all activities related to this company"
|
description: "Lists all activities related to this company"
|
||||||
operationId: getActivities
|
operationId: getActivities
|
||||||
parameters:
|
parameters:
|
||||||
@ -44,10 +44,10 @@ paths:
|
|||||||
$ref: "#/components/responses/422"
|
$ref: "#/components/responses/422"
|
||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/default"
|
$ref: "#/components/responses/default"
|
||||||
"/api/v1/actvities/download_entity/{activity_id}":
|
"/api/v1/activities/download_entity/{activity_id}":
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- actvities
|
- activities
|
||||||
summary: "Returns a PDF for the given activity"
|
summary: "Returns a PDF for the given activity"
|
||||||
description: "Returns a PDF for the given activity"
|
description: "Returns a PDF for the given activity"
|
||||||
operationId: getActivityHistoricalEntityPdf
|
operationId: getActivityHistoricalEntityPdf
|
||||||
@ -91,8 +91,6 @@ paths:
|
|||||||
summary: "Attempts authentication"
|
summary: "Attempts authentication"
|
||||||
description: "Returns a CompanyUser object on success"
|
description: "Returns a CompanyUser object on success"
|
||||||
operationId: postLogin
|
operationId: postLogin
|
||||||
security:
|
|
||||||
- []
|
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/X-API-SECRET"
|
- $ref: "#/components/parameters/X-API-SECRET"
|
||||||
- $ref: "#/components/parameters/X-API-TOKEN"
|
- $ref: "#/components/parameters/X-API-TOKEN"
|
||||||
@ -137,7 +135,7 @@ paths:
|
|||||||
422:
|
422:
|
||||||
$ref: "#/components/responses/422"
|
$ref: "#/components/responses/422"
|
||||||
5XX:
|
5XX:
|
||||||
$ref: "#/components/responses/5XX"
|
description: 'Server error'
|
||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/default"
|
$ref: "#/components/responses/default"
|
||||||
/api/v1/refresh:
|
/api/v1/refresh:
|
||||||
@ -149,7 +147,7 @@ paths:
|
|||||||
Refreshes the dataset.
|
Refreshes the dataset.
|
||||||
|
|
||||||
This endpoint can be used if you only need to access the most recent data from a certain point in time.
|
This endpoint can be used if you only need to access the most recent data from a certain point in time.
|
||||||
operationId: refresh
|
operationId: refresh
|
||||||
parameters:
|
parameters:
|
||||||
- name: updated_at
|
- name: updated_at
|
||||||
in: query
|
in: query
|
||||||
|
@ -125,12 +125,12 @@
|
|||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Client'
|
$ref: '#/components/schemas/Client'
|
||||||
example:
|
example:
|
||||||
$ref: '#/components/examples/Client'
|
$ref: '#/components/schemas/Client'
|
||||||
meta:
|
meta:
|
||||||
type: object
|
type: object
|
||||||
$ref: '#/components/schemas/Meta'
|
$ref: '#/components/schemas/Meta'
|
||||||
example:
|
example:
|
||||||
$ref: '#/components/examples/Meta'
|
$ref: '#/components/schemas/Meta'
|
||||||
401:
|
401:
|
||||||
$ref: '#/components/responses/401'
|
$ref: '#/components/responses/401'
|
||||||
403:
|
403:
|
||||||
|
@ -437,7 +437,6 @@
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
action:
|
action:
|
||||||
required: true
|
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The action to be performed, options include:
|
The action to be performed, options include:
|
||||||
@ -470,7 +469,6 @@
|
|||||||
- `send_email`
|
- `send_email`
|
||||||
Emails an array of invoices. Requires additional properties to be sent. `email_type`
|
Emails an array of invoices. Requires additional properties to be sent. `email_type`
|
||||||
ids:
|
ids:
|
||||||
required: true
|
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
description: "Array of hashed IDs to be bulk 'actioned - ['D2J234DFA','D2J234DFA','D2J234DFA']"
|
description: "Array of hashed IDs to be bulk 'actioned - ['D2J234DFA','D2J234DFA','D2J234DFA']"
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -21,7 +21,7 @@
|
|||||||
@if($account && !$account->isPaid())
|
@if($account && !$account->isPaid())
|
||||||
<div>
|
<div>
|
||||||
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}"
|
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}"
|
||||||
class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
|
class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo" id="company_logo">
|
||||||
</div>
|
</div>
|
||||||
@elseif(isset($company) && !is_null($company))
|
@elseif(isset($company) && !is_null($company))
|
||||||
<div>
|
<div>
|
||||||
|
@ -77,7 +77,7 @@ span {
|
|||||||
<td>
|
<td>
|
||||||
<div class="product-information">
|
<div class="product-information">
|
||||||
<div class="item-details">
|
<div class="item-details">
|
||||||
|
|
||||||
<p class="overflow-ellipsis overflow-hidden px-1 mb-2">{!! $product['notes'] !!}</p>
|
<p class="overflow-ellipsis overflow-hidden px-1 mb-2">{!! $product['notes'] !!}</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
@if($show_quantity)
|
@if($show_quantity)
|
||||||
@ -170,7 +170,7 @@ span {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div id="notes" class="py-10 border-b-2 border-fuschia-600" x-show="show_notes">
|
<div id="notes" class="py-10 border-b-2 border-fuschia-600" x-show="show_notes">
|
||||||
{!! html_entity_decode($entity->public_notes) !!}
|
{!! html_entity_decode(e($entity->public_notes)) !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -138,6 +138,9 @@
|
|||||||
<input type="hidden" name="action" value="payment">
|
<input type="hidden" name="action" value="payment">
|
||||||
<input type="hidden" name="company_gateway_id" value="{{ $company_gateway_id }}"/>
|
<input type="hidden" name="company_gateway_id" value="{{ $company_gateway_id }}"/>
|
||||||
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}"/>
|
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}"/>
|
||||||
|
<input type="hidden" name="contact_first_name" value="{{ $contact->first_name }}">
|
||||||
|
<input type="hidden" name="contact_last_name" value="{{ $contact->last_name }}">
|
||||||
|
<input type="hidden" name="contact_email" value="{{ $contact->email }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@if($steps['started_payment'] == false)
|
@if($steps['started_payment'] == false)
|
||||||
@ -174,7 +177,45 @@
|
|||||||
{{ ctrans('texts.trial_call_to_action') }}
|
{{ ctrans('texts.trial_call_to_action') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@elseif($steps['check_rff'])
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="alert alert-failure mb-4">
|
||||||
|
@foreach($errors->all() as $error)
|
||||||
|
<p>{{ $error }}</p>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form wire:submit="handleRff">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
@if(strlen($contact->first_name) === 0)
|
||||||
|
<div class="col-auto mt-3">
|
||||||
|
<label for="first_name" class="input-label">{{ ctrans('texts.first_name') }}</label>
|
||||||
|
<input id="first_name" class="input w-full" wire:model="contact_first_name" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(strlen($contact->last_name) === 0)
|
||||||
|
<div class="col-auto mt-3 @if($contact->last_name) !== 0) hidden @endif">
|
||||||
|
<label for="last_name" class="input-label">{{ ctrans('texts.last_name') }}</label>
|
||||||
|
<input id="last_name" class="input w-full" wire:model="contact_last_name" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(strlen($contact->email) === 0)
|
||||||
|
<div class="col-auto mt-3 @if($contact->email) !== 0) hidden @endif">
|
||||||
|
<label for="email" class="input-label">{{ ctrans('texts.email') }}</label>
|
||||||
|
<input id="email" class="input w-full" wire:model="contact_email" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button button-block bg-primary text-white mt-4">
|
||||||
|
{{ ctrans('texts.next') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
@else
|
@else
|
||||||
<form wire:submit="authenticate" class="mt-8">
|
<form wire:submit="authenticate" class="mt-8">
|
||||||
@csrf
|
@csrf
|
||||||
|
@ -1,9 +1,47 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="grid grid-cols-12 gap-8 mt-8">
|
<div class="grid grid-cols-12 gap-8 mt-8">
|
||||||
<div class="col-span-12 md:col-span-5 md:col-start-4 px-4 py-5">
|
<div class="col-span-12 md:col-span-5 md:col-start-4 px-4 py-5">
|
||||||
<!-- Total price -->
|
@if($errors->any())
|
||||||
|
<div class="alert alert-failure mb-4">
|
||||||
|
@foreach($errors->all() as $error)
|
||||||
|
<p>{{ $error }}</p>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($state['show_rff'])
|
||||||
|
<div class="mt-2">
|
||||||
|
@if(strlen(auth()->guard('contact')->user()->first_name) === 0)
|
||||||
|
<div class="col-span-6 sm:col-span-3">
|
||||||
|
<label for="first_name" class="input-label">{{ ctrans('texts.first_name') }}</label>
|
||||||
|
<input id="first_name" class="input w-full" wire:model="first_name" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if($amount > 0)
|
@if(strlen(auth()->guard('contact')->user()->last_name) === 0)
|
||||||
|
<div class="col-span-6 sm:col-span-3 mt-2">
|
||||||
|
<label for="last_name" class="input-label">{{ ctrans('texts.last_name') }}</label>
|
||||||
|
<input id="last_name" class="input w-full" wire:model="last_name" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(strlen(auth()->guard('contact')->user()->email) === 0)
|
||||||
|
<div class="col-span-6 sm:col-span-3 mt-2">
|
||||||
|
<label for="email" class="input-label">{{ ctrans('texts.email') }}</label>
|
||||||
|
<input id="email" class="input w-full" wire:model="email" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex justify-center my-4">
|
||||||
|
<button wire:click="handleRff" class="button button-primary bg-primary">
|
||||||
|
{{ ctrans('texts.next_step') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Total price -->
|
||||||
|
@if($amount > 0 && $state['show_rff'] == false)
|
||||||
|
|
||||||
<div class="relative mt-8">
|
<div class="relative mt-8">
|
||||||
<div class="absolute inset-0 flex items-center">
|
<div class="absolute inset-0 flex items-center">
|
||||||
@ -33,6 +71,9 @@
|
|||||||
<input type="hidden" name="action" value="payment">
|
<input type="hidden" name="action" value="payment">
|
||||||
<input type="hidden" name="company_gateway_id" value="{{ $state['company_gateway_id'] }}"/>
|
<input type="hidden" name="company_gateway_id" value="{{ $state['company_gateway_id'] }}"/>
|
||||||
<input type="hidden" name="payment_method_id" value="{{ $state['payment_method_id'] }}"/>
|
<input type="hidden" name="payment_method_id" value="{{ $state['payment_method_id'] }}"/>
|
||||||
|
<input type="hidden" name="contact_first_name" value="{{ $contact->first_name }}">
|
||||||
|
<input type="hidden" name="contact_last_name" value="{{ $contact->last_name }}">
|
||||||
|
<input type="hidden" name="contact_email" value="{{ $contact->email }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Payment methods -->
|
<!-- Payment methods -->
|
||||||
@ -62,7 +103,7 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@elseif($amount <= 0)
|
@elseif($amount <= 0 && $state['show_rff'] == false)
|
||||||
|
|
||||||
<div class="relative flex justify-center text-sm leading-5">
|
<div class="relative flex justify-center text-sm leading-5">
|
||||||
<h1 class="text-2xl font-bold tracking-wide bg-gray-100 px-6 py-0">
|
<h1 class="text-2xl font-bold tracking-wide bg-gray-100 px-6 py-0">
|
||||||
|
@ -98,6 +98,11 @@
|
|||||||
|
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
|
|
||||||
|
@if((bool) \App\Utils\Ninja::isSelfHost() && isset($company))
|
||||||
|
<style>
|
||||||
|
{!! $company->settings->portal_custom_css !!}
|
||||||
|
</style>
|
||||||
|
@endif
|
||||||
<link rel="stylesheet" type="text/css" href="{{ asset('vendor/cookieconsent@3/cookieconsent.min.css') }}" defer>
|
<link rel="stylesheet" type="text/css" href="{{ asset('vendor/cookieconsent@3/cookieconsent.min.css') }}" defer>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -168,6 +168,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
|
|||||||
Route::post('clients/{client}/updateTaxData', [ClientController::class, 'updateTaxData'])->name('clients.update_tax_data')->middleware('throttle:3,1');
|
Route::post('clients/{client}/updateTaxData', [ClientController::class, 'updateTaxData'])->name('clients.update_tax_data')->middleware('throttle:3,1');
|
||||||
Route::post('clients/{client}/{mergeable_client}/merge', [ClientController::class, 'merge'])->name('clients.merge')->middleware('password_protected');
|
Route::post('clients/{client}/{mergeable_client}/merge', [ClientController::class, 'merge'])->name('clients.merge')->middleware('password_protected');
|
||||||
Route::post('clients/bulk', [ClientController::class, 'bulk'])->name('clients.bulk');
|
Route::post('clients/bulk', [ClientController::class, 'bulk'])->name('clients.bulk');
|
||||||
|
Route::post('clients/{client}/documents', [ClientController::class, 'documents'])->name('clients.documents');
|
||||||
|
|
||||||
Route::post('reactivate_email/{bounce_id}', [ClientController::class, 'reactivateEmail'])->name('clients.reactivate_email');
|
Route::post('reactivate_email/{bounce_id}', [ClientController::class, 'reactivateEmail'])->name('clients.reactivate_email');
|
||||||
|
|
||||||
|
@ -59,6 +59,148 @@ class ClientApiTest extends TestCase
|
|||||||
Model::reguard();
|
Model::reguard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testClientDocumentQuery()
|
||||||
|
{
|
||||||
|
|
||||||
|
$d = \App\Models\Document::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->invoice->documents()->save($d);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson("/api/v1/clients/{$this->client->hashed_id}/documents")
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertCount(1, $arr['data']);
|
||||||
|
|
||||||
|
$d = \App\Models\Document::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->client->documents()->save($d);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson("/api/v1/clients/{$this->client->hashed_id}/documents")
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertCount(2, $arr['data']);
|
||||||
|
|
||||||
|
|
||||||
|
$d = \App\Models\Document::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->client->documents()->save($d);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson("/api/v1/clients/{$this->client->hashed_id}/documents")
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertCount(3, $arr['data']);
|
||||||
|
|
||||||
|
$d = \App\Models\Document::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->quote->documents()->save($d);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson("/api/v1/clients/{$this->client->hashed_id}/documents")
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertCount(4, $arr['data']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$d = \App\Models\Document::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->credit->documents()->save($d);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson("/api/v1/clients/{$this->client->hashed_id}/documents")
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertCount(5, $arr['data']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$d = \App\Models\Document::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
$e = \App\Models\Expense::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'client_id' => $this->client->id,
|
||||||
|
'amount' => 100
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
$e->documents()->save($d);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson("/api/v1/clients/{$this->client->hashed_id}/documents")
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertCount(6, $arr['data']);
|
||||||
|
|
||||||
|
|
||||||
|
$d = \App\Models\Document::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
$t = \App\Models\Task::factory()->create([
|
||||||
|
'company_id' => $this->company->id,
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'client_id' => $this->client->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
$t->documents()->save($d);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'X-API-TOKEN' => $this->token,
|
||||||
|
])->postJson("/api/v1/clients/{$this->client->hashed_id}/documents")
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$arr = $response->json();
|
||||||
|
|
||||||
|
$this->assertCount(7, $arr['data']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function testCrossCompanyBulkActionsFail()
|
public function testCrossCompanyBulkActionsFail()
|
||||||
{
|
{
|
||||||
|
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…
Reference in New Issue
Block a user