Merge pull request #8101 from turbo124/v5-develop

v5.5.50
This commit is contained in:
David Bomba 2023-01-02 17:56:23 +11:00 committed by GitHub
commit 03d8864652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 1170 additions and 317 deletions

View File

@ -1 +1 @@
5.5.49
5.5.50

View File

@ -119,6 +119,7 @@ class CheckData extends Command
$this->checkDuplicateRecurringInvoices();
$this->checkOauthSanity();
$this->checkVendorSettings();
$this->checkClientSettings();
if(Ninja::isHosted()){
$this->checkAccountStatuses();
@ -952,24 +953,24 @@ class CheckData extends Command
if ($this->option('fix') == 'true') {
Client::query()->whereNull('settings->currency_id')->cursor()->each(function ($client){
// Client::query()->whereNull('settings->currency_id')->cursor()->each(function ($client){
if(is_array($client->settings) && count($client->settings) == 0)
{
$settings = ClientSettings::defaults();
$settings->currency_id = $client->company->settings->currency_id;
}
else {
$settings = $client->settings;
$settings->currency_id = $client->company->settings->currency_id;
}
// if(is_array($client->settings) && count($client->settings) == 0)
// {
// $settings = ClientSettings::defaults();
// $settings->currency_id = $client->company->settings->currency_id;
// }
// else {
// $settings = $client->settings;
// $settings->currency_id = $client->company->settings->currency_id;
// }
$client->settings = $settings;
$client->save();
// $client->settings = $settings;
// $client->save();
$this->logMessage("Fixing currency for # {$client->id}");
// $this->logMessage("Fixing currency for # {$client->id}");
});
// });
Client::query()->whereNull('country_id')->cursor()->each(function ($client){

View File

@ -307,7 +307,7 @@ class CreateSingleAccount extends Command
$webhook_config = [
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
'post_purchase_rest_method' => 'POST',
'post_purchase_headers' => [],
'post_purchase_headers' => [config('ninja.ninja_hosted_header') => config('ninja.ninja_hosted_secret')],
];
$sub = SubscriptionFactory::create($company->id, $user->id);

View File

@ -23,6 +23,7 @@ use App\Jobs\Ninja\QueueSize;
use App\Jobs\Ninja\SystemMaintenance;
use App\Jobs\Ninja\TaskScheduler;
use App\Jobs\Quote\QuoteCheckExpired;
use App\Jobs\Subscription\CleanStaleInvoiceOrder;
use App\Jobs\Util\DiskCleanup;
use App\Jobs\Util\ReminderJob;
use App\Jobs\Util\SchedulerCheck;
@ -68,6 +69,9 @@ class Kernel extends ConsoleKernel
/* Sends recurring invoices*/
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new CleanStaleInvoiceOrder)->hourly()->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer();

View File

@ -93,6 +93,10 @@ class ClientRegistrationFields
'key' => 'vat_number',
'required' => false,
],
[
'key' => 'currency_id',
'required' => false,
],
];
return $data;

View File

@ -77,28 +77,45 @@ class BankTransactionFilters extends QueryFilters
$status_parameters = explode(',', $value);
$status_array = [];
$debit_or_withdrawal_array = [];
if (in_array('all', $status_parameters)) {
return $this->builder;
}
if (in_array('unmatched', $status_parameters)) {
$this->builder->where('status_id', BankTransaction::STATUS_UNMATCHED);
$status_array[] = BankTransaction::STATUS_UNMATCHED;
// $this->builder->orWhere('status_id', BankTransaction::STATUS_UNMATCHED);
}
if (in_array('matched', $status_parameters)) {
$this->builder->where('status_id', BankTransaction::STATUS_MATCHED);
$status_array[] = BankTransaction::STATUS_MATCHED;
// $this->builder->where('status_id', BankTransaction::STATUS_MATCHED);
}
if (in_array('converted', $status_parameters)) {
$this->builder->where('status_id', BankTransaction::STATUS_CONVERTED);
$status_array[] = BankTransaction::STATUS_CONVERTED;
// $this->builder->where('status_id', BankTransaction::STATUS_CONVERTED);
}
if (in_array('deposits', $status_parameters)) {
$this->builder->where('base_type', 'CREDIT');
$debit_or_withdrawal_array[] = 'CREDIT';
// $this->builder->where('base_type', 'CREDIT');
}
if (in_array('withdrawals', $status_parameters)) {
$this->builder->where('base_type', 'DEBIT');
$debit_or_withdrawal_array[] = 'DEBIT';
// $this->builder->where('base_type', 'DEBIT');
}
if(count($status_array) >=1) {
$this->builder->whereIn('status_id', $status_array);
}
if(count($debit_or_withdrawal_array) >=1) {
$this->builder->orWhereIn('base_type', $debit_or_withdrawal_array);
}
return $this->builder;

View File

@ -238,7 +238,6 @@ class ClientFilters extends QueryFilters
*/
public function entityFilter()
{
//return $this->builder->whereCompanyId(auth()->user()->company()->id);
return $this->builder->company();
}
}

View File

@ -47,20 +47,27 @@ class InvoiceFilters extends QueryFilters
$status_parameters = explode(',', $value);
$invoice_filters = [];
if (in_array('all', $status_parameters)) {
return $this->builder;
}
if (in_array('paid', $status_parameters)) {
$this->builder->where('status_id', Invoice::STATUS_PAID);
$invoice_filters[] = Invoice::STATUS_PAID;
}
if (in_array('unpaid', $status_parameters)) {
$this->builder->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]);
$invoice_filters[] = Invoice::STATUS_SENT;
$invoice_filters[] = Invoice::STATUS_PARTIAL;
}
if(count($invoice_filters) >0){
$this->builder->whereIn('status_id', $invoice_filters);
}
if (in_array('overdue', $status_parameters)) {
$this->builder->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
$this->builder->orWhereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('due_date', '<', Carbon::now())
->orWhere('partial_due_date', '<', Carbon::now());
}

View File

@ -42,20 +42,26 @@ class PurchaseOrderFilters extends QueryFilters
return $this->builder;
}
$po_status = [];
if (in_array('draft', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_DRAFT);
$po_status[] = PurchaseOrder::STATUS_DRAFT;
}
if (in_array('sent', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_SENT);
$po_status[] = PurchaseOrder::STATUS_SENT;
}
if (in_array('accepted', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_ACCEPTED);
$po_status[] = PurchaseOrder::STATUS_ACCEPTED;
}
if (in_array('cancelled', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_CANCELLED);
$po_status[] = PurchaseOrder::STATUS_CANCELLED;
}
if(count($status_parameters) >=1) {
$this->builder->whereIn('status_id', $status_parameters);
}
return $this->builder;

View File

@ -15,6 +15,7 @@ namespace App\Filters;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
/**
* Class QueryFilters.
@ -173,22 +174,30 @@ abstract class QueryFilters
}
}
public function created_at($value)
public function created_at($value = '')
{
$created_at = $value ? (int) $value : 0;
$created_at = date('Y-m-d H:i:s', $value);
if($value == '')
return $this->builder;
if(is_string($created_at)){
try{
$created_at = strtotime(str_replace("/","-",$created_at));
if(is_numeric($value)){
$created_at = Carbon::createFromTimestamp((int)$value);
}
else{
$created_at = Carbon::parse($value);
}
return $this->builder->where('created_at', '>=', $created_at);
}
catch(\Exception $e) {
if(!$created_at)
return $this->builder;
}
return $this->builder->where('created_at', '>=', $created_at);
}
public function is_deleted($value)

View File

@ -66,26 +66,32 @@ class QuoteFilters extends QueryFilters
return $this->builder;
}
$quote_filters = [];
if (in_array('draft', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_DRAFT);
$quote_filters[] = Quote::STATUS_DRAFT;
}
if (in_array('sent', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_SENT);
$quote_filters[] = Quote::STATUS_SENT;
}
if (in_array('approved', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_APPROVED);
$quote_filters[] = Quote::STATUS_APPROVED;
}
if(count($quote_filters) >=1){
$this->builder->whereIn('status_id', $quote_filters);
}
if (in_array('expired', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_SENT)
->where('due_date', '>=', now()->toDateString());
$this->builder->orWhere('status_id', Quote::STATUS_SENT)
->where('due_date', '<=', now()->toDateString());
}
if (in_array('upcoming', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_SENT)
->where('due_date', '<=', now()->toDateString())
$this->builder->orWhere('status_id', Quote::STATUS_SENT)
->where('due_date', '>=', now()->toDateString())
->orderBy('due_date', 'DESC');
}

View File

@ -51,6 +51,7 @@ class ContactRegisterController extends Controller
public function register(RegisterRequest $request)
{
$request->merge(['company' => $request->company()]);
$client = $this->getClient($request->all());
@ -58,7 +59,7 @@ class ContactRegisterController extends Controller
Auth::guard('contact')->loginUsingId($client_contact->id, true);
return redirect()->route('client.dashboard');
return redirect()->intended(route('client.dashboard'));
}
private function getClient(array $data)
@ -66,7 +67,15 @@ class ContactRegisterController extends Controller
$client = ClientFactory::create($data['company']->id, $data['company']->owner()->id);
$client->fill($data);
$client->save();
if(isset($data['currency_id'])) {
$settings = $client->settings;
$settings->currency_id = isset($data['currency_id']) ? $data['currency_id'] : $data['company']->settings->currency_id;
$client->settings = $settings;
}
$client->number = $this->getNextClientNumber($client);
$client->save();

View File

@ -52,7 +52,7 @@ class InvoiceController extends Controller
*
* @return Factory|View
*/
public function show(ShowInvoiceRequest $request, Invoice $invoice)
public function show(ShowInvoiceRequest $request, Invoice $invoice, ?string $hash = null)
{
set_time_limit(0);
@ -69,6 +69,7 @@ class InvoiceController extends Controller
'invoice' => $invoice,
'invitation' => $invitation ?: $invoice->invitations->first(),
'key' => $invitation ? $invitation->key : false,
'hash' => $hash,
];
if ($request->query('mode') === 'fullscreen') {

View File

@ -148,8 +148,17 @@ class PaymentController extends Controller
$payment = $payment->service()->applyCredits($payment_hash)->save();
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')));
event('eloquent.created: App\Models\Payment', $payment);
if($invoices->sum('balance') > 0){
$invoice = $invoices->first();
return redirect()->route('client.invoice.show', ['invoice' => $invoice->hashed_id, 'hash' => $request->input('hash')]);
}
if (property_exists($payment_hash->data, 'billing_context')) {
$billing_subscription = \App\Models\Subscription::find($payment_hash->data->billing_context->subscription_id);

View File

@ -33,7 +33,9 @@ class SubscriptionPlanSwitchController extends Controller
{
$amount = $recurring_invoice->subscription
->service()
->calculateUpgradePrice($recurring_invoice, $target);
->calculateUpgradePriceV2($recurring_invoice, $target);
nlog("payment amount = {$amount}");
/**
* Null value here is a proxy for
* denying the user a change plan option
@ -42,6 +44,9 @@ class SubscriptionPlanSwitchController extends Controller
render('subscriptions.denied');
}
$amount = max(0,$amount);
return render('subscriptions.switch', [
'subscription' => $recurring_invoice->subscription,
'recurring_invoice' => $recurring_invoice,

View File

@ -53,6 +53,16 @@ class SubscriptionPurchaseController extends Controller
$this->setLocale($request->query('locale'));
}
if(!auth()->guard('contact')->check() && $subscription->registration_required && $subscription->company->client_can_register) {
session()->put('url.intended', route('client.subscription.upgrade',['subscription' => $subscription->hashed_id]));
return redirect()->route('client.register', ['company_key' => $subscription->company->company_key]);
}
elseif(!auth()->guard('contact')->check() && $subscription->registration_required && ! $subscription->company->client_can_register) {
return render('generic.subscription_blocked', ['account' => $subscription->company->account, 'company' => $subscription->company]);
}
return view('billing-portal.purchasev2', [
'subscription' => $subscription,
'hash' => Str::uuid()->toString(),

View File

@ -521,7 +521,7 @@ class CompanyController extends BaseController
$nmo->company = $other_company;
$nmo->settings = $other_company->settings;
$nmo->to_user = auth()->user();
NinjaMailerJob::dispatch($nmo, true);
(new NinjaMailerJob($nmo, true))->handle();
$company->delete();

View File

@ -135,11 +135,45 @@ class ExpenseCategoryController extends BaseController
return $this->itemResponse($expense_category);
}
/**
* Store a newly created resource in storage.
*
* @param StoreExpenseCategoryRequest $request
* @param StoreInvoiceRequest $request The request
*
* @return Response
*
*
* @OA\Post(
* path="/api/v1/expense_categories",
* operationId="storeExpenseCategory",
* tags={"expense_categories"},
* summary="Adds a expense category",
* description="Adds an expense category to the system",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="Returns the saved invoice object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/ExpenseCategory"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function store(StoreExpenseCategoryRequest $request)
{

View File

@ -198,7 +198,7 @@ class PurchaseOrderController extends BaseController
event(new PurchaseOrderWasCreated($purchase_order, $purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
return $this->itemResponse($purchase_order);
return $this->itemResponse($purchase_order->fresh());
}
/**
* Display the specified resource.

View File

@ -174,6 +174,8 @@ class BillingPortalPurchase extends Component
*/
public $company;
public $db;
/**
* Campaign reference.
*
@ -183,7 +185,11 @@ class BillingPortalPurchase extends Component
public function mount()
{
MultiDB::setDb($this->company->db);
MultiDB::setDb($this->db);
$this->subscription = Subscription::with('company')->find($this->subscription);
$this->company = $this->subscription->company;
$this->quantity = 1;
@ -225,10 +231,10 @@ class BillingPortalPurchase extends Component
$this->steps['existing_user'] = false;
$contact = $this->createBlankClient();
$this->contact = $this->createBlankClient();
if ($contact && $contact instanceof ClientContact) {
$this->getPaymentMethods($contact);
if ($this->contact && $this->contact instanceof ClientContact) {
$this->getPaymentMethods($this->contact);
}
}
@ -265,9 +271,6 @@ class BillingPortalPurchase extends Component
}
}
// nlog($this->subscription->group_settings->settings);
// nlog($this->subscription->group_settings->settings->currency_id);
if(array_key_exists('currency_id', $this->request_data)) {
$currency = Cache::get('currencies')->filter(function ($item){

View File

@ -15,6 +15,7 @@ use App\DataMapper\ClientSettings;
use App\Factory\ClientFactory;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Subscription\CleanStaleInvoiceOrder;
use App\Libraries\MultiDB;
use App\Mail\ContactPasswordlessLogin;
use App\Mail\Subscription\OtpCode;
@ -120,7 +121,7 @@ class BillingPortalPurchasev2 extends Component
*
* @var array
*/
public $request_data;
public $request_data = [];
/**
* Instance of company.
@ -129,6 +130,14 @@ class BillingPortalPurchasev2 extends Component
*/
public $company;
/**
* Instance of company.
*
* @var string
*/
public string $db;
/**
* Campaign reference.
*
@ -151,10 +160,23 @@ class BillingPortalPurchasev2 extends Component
public $valid_coupon = false;
public $payable_invoices = [];
public $payment_confirmed = false;
public $is_eligible = true;
public $not_eligible_message = '';
public function mount()
{
MultiDB::setDb($this->company->db);
MultiDB::setDb($this->db);
$this->subscription = Subscription::with('company')->find($this->subscription);
$this->company = $this->subscription->company;
if(auth()->guard('contact')->check()){
$this->email = auth()->guard('contact')->user()->email;
$this->contact = auth()->guard('contact')->user();
$this->authenticated = true;
$this->payment_started = true;
}
$this->discount = 0;
$this->sub_total = 0;
@ -177,7 +199,7 @@ class BillingPortalPurchasev2 extends Component
$this->coupon = request()->query('coupon');
$this->handleCoupon();
}
elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){
elseif(isset($this->subscription->promo_code) && strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){
$this->price = $this->subscription->promo_price;
}
@ -224,6 +246,8 @@ class BillingPortalPurchasev2 extends Component
public function resetEmail()
{
$this->resetErrorBag('login');
$this->resetValidation('login');
$this->email = null;
}
@ -449,8 +473,6 @@ class BillingPortalPurchasev2 extends Component
$this->buildBundle();
nlog($this->bundle);
return $this;
}
@ -489,9 +511,20 @@ nlog($this->bundle);
*
* @return void
*/
public function handleBeforePaymentEvents() :void
public function handleBeforePaymentEvents() :self
{
$eligibility_check = $this->subscription->service()->isEligible($this->contact);
if(is_array($eligibility_check) && $eligibility_check['message'] != 'Success'){
$this->is_eligible = false;
$this->not_eligible_message = $eligibility_check['message'];
return $this;
}
$data = [
'client_id' => $this->contact->client->id,
'date' => now()->format('Y-m-d'),
@ -501,19 +534,9 @@ nlog($this->bundle);
]],
'user_input_promo_code' => $this->coupon,
'coupon' => empty($this->subscription->promo_code) ? '' : $this->coupon,
// 'quantity' => $this->quantity,
];
$is_eligible = $this->subscription->service()->isEligible($this->contact);
// if (is_array($is_eligible) && $is_eligible['message'] != 'Success') {
// $this->steps['not_eligible'] = true;
// $this->steps['not_eligible_message'] = $is_eligible['message'];
// $this->steps['show_loading_bar'] = false;
// return;
// }
$this->invoice = $this->subscription
->service()
->createInvoiceV2($this->bundle, $this->contact->client_id, $this->valid_coupon)
@ -534,6 +557,9 @@ nlog($this->bundle);
], now()->addMinutes(60));
$this->emit('beforePaymentEventsCompleted');
return $this;
}
public function handleTrial()

View File

@ -13,6 +13,7 @@
namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\Credit;
use App\Utils\Traits\WithSorting;
use Livewire\Component;
@ -23,26 +24,31 @@ class CreditsTable extends Component
use WithPagination;
use WithSorting;
public $per_page = 10;
public int $per_page = 10;
public $company;
public Company $company;
public string $db;
public int $company_id;
public function mount()
{
MultiDB::setDb($this->company->db);
MultiDB::setDb($this->db);
$this->company = Company::find($this->company_id);
}
public function render()
{
$query = Credit::query()
->where('client_id', auth()->guard('contact')->user()->client_id)
->where('company_id', $this->company->id)
->where('client_id', auth()->guard('contact')->user()->client_id)
->where('status_id', '<>', Credit::STATUS_DRAFT)
->where('is_deleted', 0)
->where(function ($query) {
$query->whereDate('due_date', '>=', now())
->orWhereNull('due_date');
//->orWhere('due_date', '=', '');
})
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()

View File

@ -14,6 +14,7 @@ namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Document;
use App\Models\Expense;
@ -31,21 +32,27 @@ class DocumentsTable extends Component
{
use WithPagination, WithSorting;
public $client;
public Company $company;
public $per_page = 10;
public Client $client;
public $company;
public int $client_id;
public int $per_page = 10;
public string $tab = 'documents';
public string $db;
protected $query;
public function mount($client)
public function mount()
{
MultiDB::setDb($this->company->db);
MultiDB::setDb($this->db);
$this->client = $client;
$this->client = Client::with('company')->find($this->client_id);
$this->company = $this->client->company;
$this->query = $this->documents();
}

View File

@ -13,6 +13,7 @@
namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\Invoice;
use App\Utils\Traits\WithSorting;
use Carbon\Carbon;
@ -23,15 +24,21 @@ class InvoicesTable extends Component
{
use WithPagination, WithSorting;
public $per_page = 10;
public int $per_page = 10;
public $status = [];
public array $status = [];
public $company;
public Company $company;
public int $company_id;
public string $db;
public function mount()
{
MultiDB::setDb($this->company->db);
MultiDB::setDb($this->db);
$this->company = Company::find($this->company_id);
$this->sort_asc = false;

View File

@ -23,13 +23,11 @@ class PayNowDropdown extends Component
public $company;
public function mount(int $total)
public function mount()
{
MultiDB::setDb($this->company->db);
$this->total = $total;
$this->methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods($total);
$this->methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods($this->total);
}
public function render()

View File

@ -3,7 +3,9 @@
namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\ClientGatewayToken;
use App\Models\Company;
use App\Utils\Traits\WithSorting;
use Livewire\Component;
use Livewire\WithPagination;
@ -13,17 +15,23 @@ class PaymentMethodsTable extends Component
use WithPagination;
use WithSorting;
public $per_page = 10;
public int $per_page = 10;
public $client;
public Client $client;
public $company;
public Company $company;
public function mount($client)
public int $client_id;
public string $db;
public function mount()
{
MultiDB::setDb($this->company->db);
MultiDB::setDb($this->db);
$this->client = $client;
$this->client = Client::with('company')->find($this->client_id);
$this->company = $this->client->company;
}
public function render()

View File

@ -13,6 +13,7 @@
namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\Payment;
use App\Utils\Traits\WithSorting;
use Livewire\Component;
@ -23,17 +24,19 @@ class PaymentsTable extends Component
use WithSorting;
use WithPagination;
public $per_page = 10;
public int $per_page = 10;
public $user;
public Company $company;
public $company;
public int $company_id;
public string $db;
public function mount()
{
MultiDB::setDb($this->company->db);
MultiDB::setDb($this->db);
$this->user = auth()->user();
$this->company = Company::find($this->company_id);
}
public function render()

View File

@ -13,6 +13,7 @@
namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\Quote;
use App\Utils\Traits\WithSorting;
use Livewire\Component;
@ -22,15 +23,27 @@ class QuotesTable extends Component
{
use WithPagination;
public $per_page = 10;
public int $per_page = 10;
public $status = [];
public array $status = [];
public $company;
public Company $company;
public $sort = 'status_id'; // Default sortBy. Feel free to change or pull from client/company settings.
public string $sort = 'status_id';
public bool $sort_asc = true;
public int $company_id;
public string $db;
public function mount()
{
MultiDB::setDb($this->db);
$this->company = Company::find($this->company_id);
}
public $sort_asc = true;
public function sortBy($field)
{
@ -41,16 +54,11 @@ class QuotesTable extends Component
$this->sort = $field;
}
public function mount()
{
MultiDB::setDb($this->company->db);
}
public function render()
{
$query = Quote::query()
->with('client.gateway_tokens', 'company', 'client.contacts')
->with('client.contacts', 'company')
->orderBy($this->sort, $this->sort_asc ? 'asc' : 'desc');
if (count($this->status) > 0) {

View File

@ -142,7 +142,7 @@ class SubscriptionPlanSwitch extends Component
{
$this->hide_button = true;
$response = $this->target->service()->createChangePlanCredit([
$response = $this->target->service()->createChangePlanCreditV2([
'recurring_invoice' => $this->recurring_invoice,
'subscription' => $this->subscription,
'target' => $this->target,

View File

@ -18,6 +18,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use stdClass;
class PasswordProtection
@ -111,7 +112,18 @@ class PasswordProtection
return $next($request);
}
}
elseif(auth()->user()->oauth_provider_id == 'apple')
{
$user = Socialite::driver('apple')->userFromToken($request->header('X-API-OAUTH-PASSWORD'));
if($user && ($user->email == auth()->user()->email)){
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
return $next($request);
}
}
return response()->json($error, 412);

View File

@ -47,7 +47,7 @@ class RegisterRequest extends FormRequest
foreach ($rules as $field => $properties) {
if ($field === 'email') {
$rules[$field] = array_merge($rules[$field], ['email:rfc,dns', 'max:255', Rule::unique('client_contacts')->where('company_id', $this->company()->id)]);
$rules[$field] = array_merge($rules[$field], ['email:rfc,dns', 'max:191', Rule::unique('client_contacts')->where('company_id', $this->company()->id)]);
}
if ($field === 'current_password') {

View File

@ -110,6 +110,7 @@ class UpdateCompanyRequest extends Request
}
}
if(isset($settings['email_style_custom']))
$settings['email_style_custom'] = str_replace(['{{','}}'], ['',''], $settings['email_style_custom']);
if (! $account->isFreeHostedClient()) {

View File

@ -41,7 +41,7 @@ class StoreExpenseRequest extends Request
$rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id);
}
if (! empty($this->client_id)) {
if ($this->client_id) {
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id;
}

View File

@ -41,6 +41,10 @@ class UpdateExpenseRequest extends Request
$rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->expense->id);
}
if ($this->client_id) {
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id;
}
$rules['category_id'] = 'bail|sometimes|nullable|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $this->globalRules($rules);

View File

@ -43,6 +43,7 @@ class StoreRecurringExpenseRequest extends Request
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id;
}
$rules['category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['frequency_id'] = 'required|integer|digits_between:1,12';
$rules['tax_amount1'] = 'numeric';
$rules['tax_amount2'] = 'numeric';
@ -61,10 +62,6 @@ class StoreRecurringExpenseRequest extends Request
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']);
}
if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) {
$input['currency_id'] = (string) auth()->user()->company()->settings->currency_id;
}

View File

@ -46,6 +46,7 @@ class UpdateRecurringExpenseRequest extends Request
$rules['tax_amount1'] = 'numeric';
$rules['tax_amount2'] = 'numeric';
$rules['tax_amount3'] = 'numeric';
$rules['category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $this->globalRules($rules);
}
@ -70,10 +71,6 @@ class UpdateRecurringExpenseRequest extends Request
$input['next_send_date_client'] = $input['next_send_date'];
}
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']);
}
if (array_key_exists('documents', $input)) {
unset($input['documents']);
}

View File

@ -0,0 +1,80 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Subscription;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Repositories\InvoiceRepository;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CleanStaleInvoiceOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @param int invoice_id
* @param string $db
*/
public function __construct(){}
/**
* @param InvoiceRepository $repo
* @return void
*/
public function handle(InvoiceRepository $repo) : void
{
if (! config('ninja.db.multi_db_enabled')) {
Invoice::query()
->withTrashed()
->where('is_proforma', 1)
->where('created_at', '<', now()->subHour())
->cursor()
->each(function ($invoice) use ($repo) {
$invoice->is_proforma = false;
$repo->delete($invoice);
});
return;
}
foreach (MultiDB::$dbs as $db)
{
MultiDB::setDB($db);
Invoice::query()
->withTrashed()
->where('is_proforma', 1)
->where('created_at', '<', now()->subHour())
->cursor()
->each(function ($invoice) use ($repo) {
$invoice->is_proforma = false;
$repo->delete($invoice);
});
}
}
public function failed($exception = null)
{
}
}

View File

@ -21,12 +21,10 @@ use App\Notifications\Admin\EntitySentNotification;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Contracts\Queue\ShouldQueue;
class PurchaseOrderAcceptedNotification implements ShouldQueue
class PurchaseOrderAcceptedListener implements ShouldQueue
{
use UserNotifies;
public $delay = 5;
public function __construct()
{
}

View File

@ -0,0 +1,81 @@
<?php
/**
* PurchaseOrder Ninja (https://purchase_orderninja.com).
*
* @link https://github.com/purchase_orderninja/purchase_orderninja source repository
*
* @copyright Copyright (c) 2022. PurchaseOrder Ninja LLC (https://purchase_orderninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\PurchaseOrder;
use App\Events\PurchaseOrder\PurchaseOrderWasCreated;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Admin\EntityCreatedObject;
use App\Notifications\Admin\EntitySentNotification;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Contracts\Queue\ShouldQueue;
class PurchaseOrderCreatedListener implements ShouldQueue
{
use UserNotifies;
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(PurchaseOrderWasCreated $event)
{
MultiDB::setDb($event->company->db);
$first_notification_sent = true;
$purchase_order = $event->purchase_order;
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer((new EntityCreatedObject($purchase_order, 'purchase_order'))->build());
$nmo->company = $purchase_order->company;
$nmo->settings = $purchase_order->company->settings;
/* We loop through each user and determine whether they need to be notified */
foreach ($event->company->company_users as $company_user) {
/* The User */
$user = $company_user->user;
if (! $user) {
continue;
}
/* This is only here to handle the alternate message channels - ie Slack */
// $notification = new EntitySentNotification($event->invitation, 'purchase_order');
/* Returns an array of notification methods */
$methods = $this->findUserNotificationTypes($purchase_order->invitations()->first(), $company_user, 'purchase_order', ['all_notifications', 'purchase_order_created', 'purchase_order_created_all']);
/* If one of the methods is email then we fire the EntitySentMailer */
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
$nmo->to_user = $user;
NinjaMailerJob::dispatch($nmo);
/* This prevents more than one notification being sent */
$first_notification_sent = false;
}
}
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* PurchaseOrder Ninja (https://purchase_orderninja.com).
*
* @link https://github.com/purchase_orderninja/purchase_orderninja source repository
*
* @copyright Copyright (c) 2022. PurchaseOrder Ninja LLC (https://purchase_orderninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\PurchaseOrder;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Admin\EntitySentObject;
use App\Notifications\Admin\EntitySentNotification;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Contracts\Queue\ShouldQueue;
class PurchaseOrderEmailedNotification implements ShouldQueue
{
use UserNotifies;
public $delay = 5;
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$first_notification_sent = true;
$purchase_order = $event->invitation->purchase_order->fresh();
$purchase_order->last_sent_date = now();
$purchase_order->saveQuietly();
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer((new EntitySentObject($event->invitation, 'purchase_order', 'purchase_order'))->build());
$nmo->company = $purchase_order->company;
$nmo->settings = $purchase_order->company->settings;
/* We loop through each user and determine whether they need to be notified */
foreach ($event->invitation->company->company_users as $company_user) {
/* The User */
$user = $company_user->user;
/* This is only here to handle the alternate message channels - ie Slack */
// $notification = new EntitySentNotification($event->invitation, 'purchase_order');
/* Returns an array of notification methods */
$methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'purchase_order', ['all_notifications', 'purchase_order_sent', 'purchase_order_sent_all']);
/* If one of the methods is email then we fire the EntitySentMailer */
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
$nmo->to_user = $user;
NinjaMailerJob::dispatch($nmo);
/* This prevents more than one notification being sent */
$first_notification_sent = false;
}
/* Override the methods in the Notification Class */
// $notification->method = $methods;
// Notify on the alternate channels
// $user->notify($notification);
}
}
}

View File

@ -64,7 +64,7 @@ class AutoBillingFailureObject
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$this->$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount();

View File

@ -13,6 +13,7 @@ namespace App\Mail\Admin;
use App\Utils\Ninja;
use App\Utils\Number;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Facades\App;
use stdClass;
@ -38,7 +39,11 @@ class EntityCreatedObject
$this->entity = $entity;
}
public function build()
/**
* @return stdClass
* @throws BindingResolutionException
*/
public function build() :stdClass
{
App::forgetInstance('translator');
/* Init a new copy of the translator*/
@ -47,26 +52,64 @@ class EntityCreatedObject
App::setLocale($this->entity->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->entity->company->settings));
$this->setTemplate();
$this->company = $this->entity->company;
if($this->entity_type == 'purchase_order')
{
$this->entity->load('vendor.company');
$mail_obj = new stdClass;
$mail_obj->amount = Number::formatMoney($this->entity->amount, $this->entity->vendor);
$mail_obj->subject = ctrans($this->template_subject,
[
'vendor' => $this->entity->vendor->present()->name(),
'purchase_order' => $this->entity->number,
]
);
$mail_obj->markdown = 'email.admin.generic';
$mail_obj->tag = $this->company->company_key;
$mail_obj->data = [
'title' => $mail_obj->subject,
'message' => ctrans($this->template_body,
[
'amount' => $mail_obj->amount,
'vendor' => $this->entity->vendor->present()->name(),
'purchase_order' => $this->entity->number,
]
),
'url' => $this->entity->invitations()->first()->getAdminLink(),
'button' => ctrans("texts.view_{$this->entity_type}"),
'signature' => $this->company->settings->email_signature,
'logo' => $this->company->present()->logo(),
'settings' => $this->company->settings,
'whitelabel' => $this->company->account->isPaid() ? true : false,
];
}
else {
$this->entity->load('client.country', 'client.company');
$this->client = $this->entity->client;
$this->company = $this->entity->company;
$this->setTemplate();
$mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject();
$mail_obj->data = $this->getData();
$mail_obj->markdown = 'email.admin.generic';
$mail_obj->tag = $this->company->company_key;
$mail_obj->tag = $this->entity->company->company_key;
}
return $mail_obj;
}
private function setTemplate()
{
// nlog($this->template);
switch ($this->entity_type) {
case 'invoice':
@ -81,7 +124,10 @@ class EntityCreatedObject
$this->template_subject = 'texts.notification_credit_created_subject';
$this->template_body = 'texts.notification_credit_created_body';
break;
case 'purchase_order':
$this->template_subject = 'texts.notification_purchase_order_created_subject';
$this->template_body = 'texts.notification_purchase_order_created_body';
break;
default:
$this->template_subject = 'texts.notification_invoice_created_subject';
$this->template_body = 'texts.notification_invoice_created_body';

View File

@ -58,6 +58,39 @@ class EntitySentObject
$this->setTemplate();
if($this->template == 'purchase_order')
{
$mail_obj = new stdClass;
$mail_obj->amount = Number::formatMoney($this->entity->amount, $this->entity->vendor);
$mail_obj->subject = ctrans($this->template_subject,
[
'vendor' => $this->contact->vendor->present()->name(),
'purchase_order' => $this->entity->number,
]
);
$mail_obj->data = [
'title' => $mail_obj->subject,
'message' => ctrans($this->template_body,
[
'amount' => $mail_obj->amount,
'vendor' => $this->contact->vendor->present()->name(),
'purchase_order' => $this->entity->number,
]
),
'url' => $this->invitation->getAdminLink(),
'button' => ctrans("texts.view_{$this->entity_type}"),
'signature' => $this->company->settings->email_signature,
'logo' => $this->company->present()->logo(),
'settings' => $this->company->settings,
'whitelabel' => $this->company->account->isPaid() ? true : false,
];
$mail_obj->markdown = 'email.admin.generic';
$mail_obj->tag = $this->company->company_key;
}
else {
$mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject();
@ -65,6 +98,8 @@ class EntitySentObject
$mail_obj->markdown = 'email.admin.generic';
$mail_obj->tag = $this->company->company_key;
}
return $mail_obj;
}
@ -101,7 +136,10 @@ class EntitySentObject
$this->template_subject = 'texts.notification_credit_sent_subject';
$this->template_body = 'texts.notification_credit_sent';
break;
case 'purchase_order':
$this->template_subject = 'texts.notification_purchase_order_sent_subject';
$this->template_body = 'texts.notification_purchase_order_sent';
break;
default:
$this->template_subject = 'texts.notification_invoice_sent_subject';
$this->template_body = 'texts.notification_invoice_sent';

View File

@ -129,6 +129,7 @@ class Company extends BaseModel
'invoice_task_lock',
'convert_payment_currency',
'convert_expense_currency',
'notify_vendor_when_paid',
];
protected $hidden = [
@ -138,6 +139,7 @@ class Company extends BaseModel
];
protected $casts = [
'is_proforma' => 'bool',
'country_id' => 'string',
'custom_fields' => 'object',
'settings' => 'object',

View File

@ -117,6 +117,11 @@ class Vendor extends BaseModel
}
public function timezone()
{
return $this->company->timezone();
}
public function company()
{
return $this->belongsTo(Company::class);

View File

@ -325,15 +325,6 @@ class BaseDriver extends AbstractPaymentDriver
$invoice->service()->toggleFeesPaid()->save();
}
$transaction = [
'invoice' => $invoice->transaction_event(),
'payment' => [],
'client' => $invoice->client->transaction_event(),
'credit' => [],
'metadata' => [],
];
// TransactionLog::dispatch(TransactionEvent::INVOICE_FEE_APPLIED, $transaction, $invoice->company->db);
});
}

View File

@ -124,18 +124,20 @@ class CreditCard implements MethodInterface
}
} catch (CheckoutApiException $e) {
// API error
$request_id = $e->request_id;
$http_status_code = $e->http_status_code;
$request_id = $e->request_id ?: '';
$http_status_code = $e->http_status_code ?: '';
$error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
$human_exception = $error_details ? $error_details : $e->getMessage();
$human_exception = "{$human_exception} - Request ID: {$request_id}";
throw new PaymentFailed($human_exception, $http_status_code);
throw new PaymentFailed($human_exception);
} catch (CheckoutArgumentException $e) {
// Bad arguments
@ -145,9 +147,9 @@ class CreditCard implements MethodInterface
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
$human_exception = $error_details ? $error_details : $e->getMessage();
throw new PaymentFailed($human_exception);
throw new PaymentFailed($human_exception, 422);
} catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization
@ -157,9 +159,9 @@ class CreditCard implements MethodInterface
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
$human_exception = $error_details ? $error_details : $e->getMessage();
throw new PaymentFailed($human_exception);
throw new PaymentFailed($human_exception, 401);
}
}
@ -228,7 +230,7 @@ class CreditCard implements MethodInterface
private function completePayment($paymentRequest, PaymentResponseRequest $request)
{
$paymentRequest->amount = $this->checkout->payment_hash->data->value;
$paymentRequest->reference = $this->checkout->getDescription();
$paymentRequest->reference = substr($this->checkout->getDescription(),0 , 49);
$paymentRequest->customer = $this->checkout->getCustomer();
$paymentRequest->metadata = ['udf1' => 'Invoice Ninja'];
$paymentRequest->currency = $this->checkout->client->getCurrencyCode();

View File

@ -87,6 +87,9 @@ trait Utilities
$error_message = '';
nlog("checkout failure");
nlog($_payment);
if (is_array($_payment) && array_key_exists('actions', $_payment) && array_key_exists('response_summary', end($_payment['actions']))) {
$error_message = end($_payment['actions'])['response_summary'];
} elseif (is_array($_payment) && array_key_exists('status', $_payment)) {

View File

@ -627,18 +627,16 @@ class StripePaymentDriver extends BaseDriver
public function processWebhookRequest(PaymentWebhookRequest $request)
{
// Allow app to catch up with webhook request.
sleep(2);
//payment_intent.succeeded - this will confirm or cancel the payment
if ($request->type === 'payment_intent.succeeded') {
PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2, 10)));
PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10)));
return response()->json([], 200);
}
if (in_array($request->type, ['payment_intent.payment_failed', 'charge.failed'])) {
PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2, 10)));
PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10)));
return response()->json([], 200);
}

View File

@ -157,15 +157,14 @@ use App\Listeners\Credit\CreditRestoredActivity;
use App\Listeners\Credit\CreditViewedActivity;
use App\Listeners\Document\DeleteCompanyDocuments;
use App\Listeners\Invoice\CreateInvoiceActivity;
use App\Listeners\Invoice\CreateInvoiceHtmlBackup;
use App\Listeners\Invoice\CreateInvoicePdf;
use App\Listeners\Invoice\InvoiceArchivedActivity;
use App\Listeners\Invoice\InvoiceCancelledActivity;
use App\Listeners\Invoice\InvoiceCreatedNotification;
use App\Listeners\Invoice\InvoiceDeletedActivity;
use App\Listeners\Invoice\InvoiceEmailActivity;
use App\Listeners\Invoice\InvoiceEmailedNotification;
use App\Listeners\Invoice\InvoiceEmailFailedActivity;
use App\Listeners\Invoice\InvoiceEmailedNotification;
use App\Listeners\Invoice\InvoiceFailedEmailNotification;
use App\Listeners\Invoice\InvoicePaidActivity;
use App\Listeners\Invoice\InvoiceReminderEmailActivity;
@ -175,18 +174,21 @@ use App\Listeners\Invoice\InvoiceViewedActivity;
use App\Listeners\Invoice\UpdateInvoiceActivity;
use App\Listeners\Mail\MailSentListener;
use App\Listeners\Misc\InvitationViewedListener;
use App\Listeners\Payment\PaymentEmailedActivity;
use App\Listeners\Payment\PaymentEmailFailureActivity;
use App\Listeners\Payment\PaymentEmailedActivity;
use App\Listeners\Payment\PaymentNotification;
use App\Listeners\Payment\PaymentRestoredActivity;
use App\Listeners\PurchaseOrder\CreatePurchaseOrderActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedNotification;
use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedListener;
use App\Listeners\PurchaseOrder\PurchaseOrderArchivedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderCreatedListener;
use App\Listeners\PurchaseOrder\PurchaseOrderDeletedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderEmailActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderEmailedNotification;
use App\Listeners\PurchaseOrder\PurchaseOrderRestoredActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderViewedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderViewedNotification;
use App\Listeners\PurchaseOrder\UpdatePurchaseOrderActivity;
use App\Listeners\Quote\QuoteApprovedActivity;
use App\Listeners\Quote\QuoteApprovedNotification;
@ -219,8 +221,8 @@ use App\Listeners\User\ArchivedUserActivity;
use App\Listeners\User\CreatedUserActivity;
use App\Listeners\User\DeletedUserActivity;
use App\Listeners\User\RestoredUserActivity;
use App\Listeners\User\UpdatedUserActivity;
use App\Listeners\User\UpdateUserLastLogin;
use App\Listeners\User\UpdatedUserActivity;
use App\Models\Account;
use App\Models\Client;
use App\Models\Company;
@ -398,7 +400,6 @@ class EventServiceProvider extends ServiceProvider
],
//Invoices
InvoiceWasMarkedSent::class => [
CreateInvoiceHtmlBackup::class,
],
InvoiceWasUpdated::class => [
UpdateInvoiceActivity::class,
@ -458,12 +459,14 @@ class EventServiceProvider extends ServiceProvider
],
PurchaseOrderWasCreated::class => [
CreatePurchaseOrderActivity::class,
PurchaseOrderCreatedListener::class,
],
PurchaseOrderWasDeleted::class => [
PurchaseOrderDeletedActivity::class,
],
PurchaseOrderWasEmailed::class => [
PurchaseOrderEmailActivity::class,
PurchaseOrderEmailedNotification::class,
],
PurchaseOrderWasRestored::class => [
PurchaseOrderRestoredActivity::class,
@ -475,8 +478,8 @@ class EventServiceProvider extends ServiceProvider
PurchaseOrderViewedActivity::class,
],
PurchaseOrderWasAccepted::class => [
PurchaseOrderAcceptedListener::class,
PurchaseOrderAcceptedActivity::class,
PurchaseOrderAcceptedNotification::class,
],
CompanyDocumentsDeleted::class => [
DeleteCompanyDocuments::class,

View File

@ -21,6 +21,7 @@ use App\Models\Invoice;
use App\Services\Bank\BankService;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
@ -29,7 +30,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
class BankMatchingService implements ShouldQueue
class BankMatchingService implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -37,13 +38,10 @@ class BankMatchingService implements ShouldQueue
protected $db;
protected $middleware_key;
public function __construct($company_id, $db)
{
$this->company_id = $company_id;
$this->db = $db;
$this->middleware_key = "bank_match_rate:{$this->company_id}";
}
public function handle() :void
@ -62,8 +60,14 @@ class BankMatchingService implements ShouldQueue
}
public function middleware()
/**
* The unique ID of the job.
*
* @return string
*/
public function uniqueId()
{
return [new WithoutOverlapping($this->middleware_key)];
return (string)$this->company_id;
}
}

View File

@ -48,7 +48,7 @@ class InstantPayment
public function run()
{
nlog($this->request->all());
$is_credit_payment = false;
$tokens = [];
@ -221,6 +221,9 @@ class InstantPayment
if ($this->request->query('hash')) {
$hash_data['billing_context'] = Cache::get($this->request->query('hash'));
}
elseif($this->request->hash){
$hash_data['billing_context'] = Cache::get($this->request->hash);
}
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(32);

View File

@ -82,9 +82,6 @@ class MarkInvoiceDeleted extends AbstractService
{
//if total payments = adjustment amount - that means we need to delete the payments as well.
nlog($this->adjustment_amount);
nlog($this->total_payments);
if ($this->adjustment_amount == $this->total_payments)
$this->invoice->payments()->update(['payments.deleted_at' => now(), 'payments.is_deleted' => true]);

View File

@ -140,6 +140,39 @@ class PaymentService
return $this;
}
public function applyCreditsToInvoice($invoice)
{
$amount = $invoice->amount;
$credits = $invoice->client
->service()
->getCredits();
foreach ($credits as $credit) {
//starting invoice balance
$invoice_balance = $invoice->balance;
//credit payment applied
$credit->service()->applyPayment($invoice, $amount, $this->payment);
//amount paid from invoice calculated
$remaining_balance = ($invoice_balance - $invoice->fresh()->balance);
//reduce the amount to be paid on the invoice from the NEXT credit
$amount -= $remaining_balance;
//break if the invoice is no longer PAYABLE OR there is no more amount to be applied
if (! $invoice->isPayable() || (int) $amount == 0) {
break;
}
}
return $this;
}
public function save()
{
$this->payment->saveQuietly();

View File

@ -80,12 +80,20 @@ class UpdateInvoicePayment
->clearPartial()
->updateStatus()
->touchPdf()
->save();
$invoice->service()
->workFlow()
->save();
if($invoice->is_proforma)
{
$invoice->number = '';
$invoice->is_proforma = false;
$invoice->service()
->applyNumber()
->save();
}
/* Updates the company ledger */
$this->payment
->ledger()
@ -101,17 +109,6 @@ class UpdateInvoicePayment
$this->payment->applied += $paid_amount;
$transaction = [
'invoice' => $invoice->transaction_event(),
'payment' => $this->payment->transaction_event(),
'client' => $client->transaction_event(),
'credit' => [],
'metadata' => [],
];
// TransactionLog::dispatch(TransactionEvent::GATEWAY_PAYMENT_MADE, $transaction, $invoice->company->db);
});
/* Remove the event updater from within the loop to prevent race conditions */

View File

@ -40,7 +40,7 @@ class MarkSent
->service()
->setStatus(PurchaseOrder::STATUS_SENT)
->applyNumber()
// ->adjustBalance($this->purchase_order->amount)
->adjustBalance($this->purchase_order->amount) //why was this commented out previously?
// ->touchPdf()
->save();

View File

@ -39,6 +39,7 @@ class PurchaseOrderExpense
$expense->uses_inclusive_taxes = $this->purchase_order->uses_inclusive_taxes;
$expense->calculate_tax_by_amount = true;
$expense->private_notes = ctrans('texts.purchase_order_number_short') . " " . $this->purchase_order->number;
$expense->currency_id = $this->purchase_order->vendor->currency_id;
$line_items = $this->purchase_order->line_items;

View File

@ -97,6 +97,13 @@ class PurchaseOrderService
return $this;
}
public function adjustBalance($adjustment)
{
$this->purchase_order->balance += $adjustment;
return $this;
}
public function touchPdf($force = false)
{
try {

View File

@ -15,6 +15,7 @@ use App\DataMapper\InvoiceItem;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceToRecurringInvoiceFactory;
use App\Factory\PaymentFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
@ -28,6 +29,7 @@ use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\Product;
use App\Models\RecurringInvoice;
use App\Models\Subscription;
@ -89,11 +91,18 @@ class SubscriptionService
$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::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',
@ -101,7 +110,7 @@ class SubscriptionService
'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()->first()->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,
];
@ -217,23 +226,70 @@ class SubscriptionService
*
* @return float
*/
public function calculateUpgradePriceV2(RecurringInvoice $recurring_invoice, Subscription $target) :?float
{
$outstanding_credit = 0;
$use_credit_setting = $recurring_invoice->client->getSetting('use_credits_payment');
$last_invoice = Invoice::query()
->where('recurring_id', $recurring_invoice->id)
->where('is_deleted', 0)
->where('status_id', Invoice::STATUS_PAID)
->first();
$refund = $this->calculateProRataRefundForSubscription($last_invoice);
if($use_credit_setting != 'off')
{
$outstanding_credit = Credit::query()
->where('client_id', $recurring_invoice->client_id)
->whereIn('status_id', [Credit::STATUS_SENT,Credit::STATUS_PARTIAL])
->where('is_deleted', 0)
->where('balance', '>', 0)
->sum('balance');
}
nlog("{$target->price} - {$refund} - {$outstanding_credit}");
return $target->price - $refund - $outstanding_credit;
}
/**
* Returns an upgrade price when moving between plans
*
* However we only allow people to move between plans
* if their account is in good standing.
*
* @param RecurringInvoice $recurring_invoice
* @param Subscription $target
* @deprecated in favour of calculateUpgradePriceV2
* @return float
*/
public function calculateUpgradePrice(RecurringInvoice $recurring_invoice, Subscription $target) :?float
{
//calculate based on daily prices
//calculate based on daily prices
$current_amount = $recurring_invoice->amount;
$currency_frequency = $recurring_invoice->frequency_id;
$outstanding = $recurring_invoice->invoices()
$outstanding = Invoice::query()
->where('recurring_id', $recurring_invoice->id)
->where('is_deleted', 0)
->where('is_proforma',0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0);
$outstanding_amounts = $outstanding->sum('balance');
$outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id)
->where('client_id', $recurring_invoice->client_id)
$outstanding_invoice = Invoice::where('client_id', $recurring_invoice->client_id)
->where('is_deleted', 0)
->where('is_proforma',0)
->where('subscription_id', $this->subscription->id)
->orderBy('id', 'desc')
->first();
@ -242,6 +298,7 @@ class SubscriptionService
$outstanding_invoice = Credit::where('subscription_id', $this->subscription->id)
->where('client_id', $recurring_invoice->client_id)
->where('is_proforma',0)
->where('is_deleted', 0)
->orderBy('id', 'desc')
->first();
@ -289,13 +346,9 @@ class SubscriptionService
$days_in_frequency = $this->getDaysInFrequency();
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $this->subscription->price ,2);
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2);
// nlog("days in frequency = {$days_in_frequency} - days of subscription used {$days_of_subscription_used}");
// nlog("invoice amount = {$invoice->amount}");
// nlog("pro rata refund = {$pro_rata_refund}");
return $pro_rata_refund;
return max(0, $pro_rata_refund);
}
@ -323,10 +376,6 @@ class SubscriptionService
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2);
// nlog("days in frequency = {$days_in_frequency} - days of subscription used {$days_of_subscription_used}");
// nlog("invoice amount = {$invoice->amount}");
// nlog("pro rata refund = {$pro_rata_refund}");
return $pro_rata_refund;
}
@ -353,7 +402,6 @@ class SubscriptionService
$days_of_subscription_used = $start_date->diffInDays($current_date);
// $days_in_frequency = $this->getDaysInFrequency();
$days_in_frequency = $invoice->subscription->service()->getDaysInFrequency();
$ratio = ($days_in_frequency - $days_of_subscription_used)/$days_in_frequency;
@ -406,10 +454,76 @@ class SubscriptionService
return $pro_rata_charge;
}
/**
* This entry point assumes the user does not have to make a
* payment for the service.
*
* In this case, we generate a credit note for the old service
* Generate a new invoice for the new service
* Apply credits to the invoice
*
* @param array $data
*/
public function createChangePlanCreditV2($data)
{
/* Init vars */
$recurring_invoice = $data['recurring_invoice'];
$old_subscription = $data['subscription'];
$target_subscription = $data['target'];
$pro_rata_charge_amount = 0;
$pro_rata_refund_amount = 0;
$is_credit = false;
$credit = false;
/* Get last invoice */
$last_invoice = Invoice::where('subscription_id', $recurring_invoice->subscription_id)
->where('client_id', $recurring_invoice->client_id)
->where('is_proforma',0)
->where('is_deleted', 0)
->where('status_id', Invoice::STATUS_PAID)
->withTrashed()
->orderBy('id', 'desc')
->first();
if($this->calculateProRataRefundForSubscription($last_invoice) > 0)
$credit = $this->createCredit($last_invoice, $target_subscription, false);
$new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice);
$invoice = $this->changePlanInvoice($target_subscription, $recurring_invoice->client_id);
$invoice->recurring_id = $new_recurring_invoice->id;
$invoice->save();
$payment = PaymentFactory::create($invoice->company_id, $invoice->user_id, $invoice->client_id);
$payment->type_id = PaymentType::CREDIT;
$payment->client_id = $invoice->client_id;
$payment->is_manual = true;
$payment->save();
$payment->service()->applyCreditsToInvoice($invoice);
$context = [
'context' => 'change_plan',
'recurring_invoice' => $new_recurring_invoice->hashed_id,
'credit' => $credit ? $credit->hashed_id : null,
'client' => $new_recurring_invoice->client->hashed_id,
'subscription' => $target_subscription->hashed_id,
'contact' => auth()->guard('contact')->user()->hashed_id,
'account_key' => $new_recurring_invoice->client->custom_value2,
];
$response = $this->triggerWebhook($context);
return '/client/recurring_invoices/'.$new_recurring_invoice->hashed_id;
}
/**
* When downgrading, we may need to create
* a credit
*
* @deprecated in favour of createChangePlanCreditV2
* @param array $data
*/
public function createChangePlanCredit($data)
@ -663,10 +777,10 @@ class SubscriptionService
$credit = CreditFactory::create($this->subscription->company_id, $this->subscription->user_id);
$credit->date = now()->format('Y-m-d');
$credit->subscription_id = $this->subscription->id;
$credit->discount = $last_invoice->discount;
$credit->is_amount_discount = $last_invoice->is_amount_discount;
$line_items = $subscription_repo->generateLineItems($target, false, true);
$credit->line_items = array_merge($line_items, $this->calculateProRataRefundItems($last_invoice, $last_invoice_is_credit));
$credit->line_items = $this->calculateProRataRefundItems($last_invoice, true);
$data = [
'client_id' => $last_invoice->client_id,
@ -696,6 +810,7 @@ class SubscriptionService
$invoice->subscription_id = $target->id;
$invoice->line_items = array_merge($subscription_repo->generateLineItems($target), $this->calculateProRataRefundItems($last_invoice));
$invoice->is_proforma = true;
$data = [
'client_id' => $client_id,
@ -711,6 +826,40 @@ class SubscriptionService
}
/**
* When changing plans we need to generate a pro rata
* invoice which takes into account any credits.
*
* @param Subscription $target
* @return Invoice
*/
private function changePlanInvoice($target, $client_id)
{
$subscription_repo = new SubscriptionRepository();
$invoice_repo = new InvoiceRepository();
$invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
$invoice->date = now()->format('Y-m-d');
$invoice->subscription_id = $target->id;
$invoice->line_items = $subscription_repo->generateLineItems($target);
$invoice->is_proforma = true;
$data = [
'client_id' => $client_id,
'quantity' => 1,
'date' => now()->format('Y-m-d'),
];
return $invoice_repo->save($data, $invoice)
->service()
->markSent()
->fillDefaults()
->save();
}
public function createInvoiceV2($bundle, $client_id, $valid_coupon = false)
{
@ -720,7 +869,8 @@ class SubscriptionService
$invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
$invoice->subscription_id = $this->subscription->id;
$invoice->client_id = $client_id;
$invoice->is_proforma = true;
$invoice->number = ctrans('texts.subscription') . "_" . now()->format('Y-m-d') . "_" . rand(0,100000);
$line_items = $bundle->map(function ($item){
$line_item = new InvoiceItem;
@ -760,6 +910,7 @@ class SubscriptionService
$invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
$invoice->line_items = $subscription_repo->generateLineItems($this->subscription);
$invoice->subscription_id = $this->subscription->id;
$invoice->is_proforman = true;
if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0)
{
@ -771,7 +922,6 @@ class SubscriptionService
$invoice->is_amount_discount = $this->subscription->is_amount_discount;
}
return $invoice_repo->save($data, $invoice);
}
@ -860,14 +1010,11 @@ class SubscriptionService
*/
public function triggerWebhook($context)
{
nlog("trigger webhook");
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];
}
nlog("past first if");
$response = false;
$body = array_merge($context, [
@ -876,8 +1023,6 @@ class SubscriptionService
$response = $this->sendLoad($this->subscription, $body);
nlog("after response");
/* Append the response to the system logger body */
if(is_array($response)){
@ -1098,8 +1243,6 @@ class SubscriptionService
});
return $this->handleRedirect('client/subscriptions');
}

View File

@ -193,6 +193,7 @@ class CompanyTransformer extends EntityTransformer
'invoice_task_lock' => (bool) $company->invoice_task_lock,
'convert_payment_currency' => (bool) $company->convert_payment_currency,
'convert_expense_currency' => (bool) $company->convert_expense_currency,
'notify_vendor_when_paid' => (bool) $company->notify_vendor_when_paid,
];
}

View File

@ -15,6 +15,7 @@ use App\Models\Document;
use App\Models\RecurringExpense;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\SoftDeletes;
use League\Fractal\Resource\Item;
/**
* class RecurringExpenseTransformer.
@ -33,6 +34,8 @@ class RecurringExpenseTransformer extends EntityTransformer
*/
protected $availableIncludes = [
'documents',
'client',
'vendor',
];
public function includeDocuments(RecurringExpense $recurring_expense)
@ -42,6 +45,28 @@ class RecurringExpenseTransformer extends EntityTransformer
return $this->includeCollection($recurring_expense->documents, $transformer, Document::class);
}
public function includeClient(RecurringExpense $recurring_expense): ?Item
{
$transformer = new ClientTransformer($this->serializer);
if (!$recurring_expense->client) {
return null;
}
return $this->includeItem($recurring_expense->client, $transformer, Client::class);
}
public function includeVendor(RecurringExpense $recurring_expense): ?Item
{
$transformer = new VendorTransformer($this->serializer);
if (!$recurring_expense->vendor) {
return null;
}
return $this->includeItem($recurring_expense->vendor, $transformer, Vendor::class);
}
/**
* @param RecurringExpense $recurring_expense
*

View File

@ -129,12 +129,12 @@ class Helpers
if(!$string_hit)
return $value;
// 04-10-2022 Return Early if no reserved keywords are present, this is a very expensive process
// 04-10-2022 Return Early if no reserved keywords are present, this is a very expensive process
Carbon::setLocale($entity->locale());
if (!$currentDateTime) {
$currentDateTime = Carbon::now();
$currentDateTime = Carbon::now()->timezone($entity->timezone()->name);
}
$replacements = [

View File

@ -66,7 +66,8 @@ trait GeneratesCounter
$counter = 1;
}
$counter_entity = $client->group_settings;
// $counter_entity = $client->group_settings;
$counter_entity = $client->group_settings ?: $client->company;
} else {
$counter = $client->company->settings->{$counter_string};
$counter_entity = $client->company;

View File

@ -25,7 +25,7 @@ class PDF extends FPDI
$this->SetTextColor(135, 135, 135);
$trans = ctrans('texts.pdf_page_info', ['current' => $this->PageNo(), 'total' => '{nb}']);
$trans = iconv('UTF-8', 'ISO-8859-7', $trans);
// $trans = iconv('UTF-8', 'ISO-8859-7', $trans);
$this->Cell(0, 5, $trans, 0, 0, $this->text_alignment);
}

View File

@ -52,7 +52,7 @@ trait SettingsSaver
continue;
}
/*Separate loop if it is a _id field which is an integer cast as a string*/
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && property_exists($settings, $key) && strlen($settings->{$key}) >= 1)) {
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && property_exists($settings, $key) && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && property_exists($settings, $key) && strlen($settings->{$key}) >= 1)) {
$value = 'integer';
if($key == 'gmail_sending_user_id' || $key == 'besr_id')

View File

@ -12,6 +12,8 @@
namespace App\Utils\Traits;
use GuzzleHttp\RequestOptions;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Message;
/**
* Class SubscriptionHooker.
@ -34,10 +36,6 @@ trait SubscriptionHooker
'headers' => $headers,
]);
nlog('method name must be a string');
nlog($subscription->webhook_configuration['post_purchase_rest_method']);
nlog($subscription->webhook_configuration['post_purchase_url']);
$post_purchase_rest_method = (string) $subscription->webhook_configuration['post_purchase_rest_method'];
$post_purchase_url = (string) $subscription->webhook_configuration['post_purchase_url'];
@ -47,7 +45,18 @@ trait SubscriptionHooker
]);
return array_merge($body, json_decode($response->getBody(), true));
} catch (\Exception $e) {
} catch (ClientException $e) {
$message = $e->getMessage();
$error = json_decode($e->getResponse()->getBody()->getContents());
if(property_exists($error, 'message'))
$message = $error->message;
return array_merge($body, ['message' => $message, 'status_code' => 500]);
}
catch (\Exception $e) {
return array_merge($body, ['message' => $e->getMessage(), 'status_code' => 500]);
}
}

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.49',
'app_tag' => '5.5.49',
'app_version' => '5.5.50',
'app_tag' => '5.5.50',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),
@ -190,7 +190,8 @@ return [
'ninja_stripe_client_id' => env('NINJA_STRIPE_CLIENT_ID', null),
'ninja_default_company_id' => env('NINJA_COMPANY_ID', null),
'ninja_default_company_gateway_id' => env('NINJA_COMPANY_GATEWAY_ID', null),
'ninja_hosted_secret' => env('NINJA_HOSTED_SECRET', null),
'ninja_hosted_secret' => env('NINJA_HOSTED_SECRET', ''),
'ninja_hosted_header' =>env('NINJA_HEADER',''),
'internal_queue_enabled' => env('INTERNAL_QUEUE_ENABLED', true),
'ninja_apple_api_key' => env('APPLE_API_KEY', false),
'ninja_apple_private_key' => env('APPLE_PRIVATE_KEY', false),

View File

@ -29,14 +29,8 @@ class ProductFactory extends Factory
'cost' => $this->faker->numberBetween(1, 1000),
'price' => $this->faker->numberBetween(1, 1000),
'quantity' => $this->faker->numberBetween(1, 100),
// 'tax_name1' => 'GST',
// 'tax_rate1' => 10,
// 'tax_name2' => 'VAT',
// 'tax_rate2' => 17.5,
// 'tax_name3' => 'THIRDTAX',
// 'tax_rate3' => 5,
'custom_value1' => $this->faker->text(20),
'custom_value2' => $this->faker->text(20),
'custom_value1' => 'https://picsum.photos/200',
'custom_value2' => rand(0,100),
'custom_value3' => $this->faker->text(20),
'custom_value4' => $this->faker->text(20),
'is_deleted' => false,

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('companies', function (Blueprint $table)
{
$table->boolean('notify_vendor_when_paid')->default(false);
});
Schema::table('invoices', function (Blueprint $table)
{
$table->boolean('is_proforma')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

View File

@ -4301,7 +4301,7 @@ $LANG = array(
'becs_mandate' => 'By providing your bank account details, you agree to this <a class="underline" href="https://stripe.com/au-becs-dd-service-agreement/legal">Direct Debit Request and the Direct Debit Request service agreement</a>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.',
'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.',
'direct_debit' => 'Direct Debit',
'clone_to_expense' => 'Clone to expense',
'clone_to_expense' => 'Clone to Expense',
'checkout' => 'Checkout',
'acss' => 'Pre-authorized debit payments',
'invalid_amount' => 'Invalid amount. Number/Decimal values only.',
@ -4906,7 +4906,12 @@ $LANG = array(
'backup_restore' => 'Backup | Restore',
'export_company' => 'Create company backup',
'backup' => 'Backup',
'notification_purchase_order_created_body' => 'The following purchase_order :purchase_order was created for vendor :vendor for :amount.',
'notification_purchase_order_created_subject' => 'Purchase Order :purchase_order was created for :vendor',
'notification_purchase_order_sent_subject' => 'Purchase Order :purchase_order was sent to :vendor',
'notification_purchase_order_sent' => 'The following vendor :vendor was emailed Purchase Order :purchase_order for :amount.',
'subscription_blocked' => 'This product is a restricted item, please contact the vendor for further information.',
'subscription_blocked_title' => 'Product not available.',
);
return $LANG;

View File

@ -2,7 +2,7 @@
@section('meta_title', ctrans('texts.purchase'))
@section('body')
@livewire('billing-portal-purchase', ['subscription' => $subscription, 'company' => $subscription->company, 'contact' => auth()->guard('contact')->user(), 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null])
@livewire('billing-portal-purchase', ['subscription' => $subscription->id, 'db' => $subscription->company->db, 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null])
@stop
@push('footer')

View File

@ -2,7 +2,7 @@
@section('meta_title', ctrans('texts.purchase'))
@section('body')
@livewire('billing-portal-purchasev2', ['subscription' => $subscription, 'company' => $subscription->company, 'contact' => auth()->guard('contact')->user(), 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null])
@livewire('billing-portal-purchasev2', ['subscription' => $subscription->id, 'db' => $subscription->company->db, 'hash' => $hash, 'request_data' => $request_data, 'campaign' => request()->query('campaign') ?? null])
@stop
@push('footer')

View File

@ -5,13 +5,21 @@
<div class="grid lg:grid-cols-12 py-8">
<div class="col-span-12 lg:col-span-8 lg:col-start-3 xl:col-span-6 xl:col-start-4 px-6">
@if($register_company->account && !$register_company->account->isPaid())
<div class="flex justify-center">
<img class="h-32 w-auto" src="{{ $register_company->present()->logo() }}" alt="{{ ctrans('texts.logo') }}">
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}"
class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div>
@elseif(isset($register_company) && !is_null($register_company))
<div class="flex justify-center">
<img src="{{ $register_company->present()->logo() }}"
class="mx-auto border-b border-gray-100 h-18 pb-4" alt="{{ $register_company->present()->name() }} logo">
</div>
@endif
<h1 class="text-center text-3xl mt-8">{{ ctrans('texts.register') }}</h1>
<p class="block text-center text-gray-600">{{ ctrans('texts.register_label') }}</p>
<form action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{more: false, busy: false, isSubmitted: false}" x-on:submit="isSubmitted = true">
<form id="register-form" action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{more: false, busy: false, isSubmitted: false}" x-on:submit="isSubmitted = true">
@if($register_company)
<input type="hidden" name="company_key" value="{{ $register_company->company_key }}">
@endif
@ -54,6 +62,18 @@
type="password"
name="{{ $field['key'] }}"
/>
@elseif($field['key'] === 'currency_id')
<select
id="currency_id"
class="input w-full form-select bg-white"
name="currency_id">
@foreach(App\Utils\TranslationHelper::getCurrencies() as $currency)
<option
{{ $currency->id == $register_company->settings->currency_id ? 'selected' : null }} value="{{ $currency->id }}">
{{ $currency->name }}
</option>
@endforeach
</select>
@elseif($field['key'] === 'country_id')
<select
id="shipping_country"
@ -112,6 +132,9 @@
</div>
<div class="flex justify-between items-center mt-8">
<a href="{{route('client.login')}}" class="button button-info bg-green-600 text-white">{{ ctrans('texts.login_label') }}</a>
<span class="inline-flex items-center" x-data="{ terms_of_service: false, privacy_policy: false }">
@if(!empty($register_company->settings->client_portal_terms) || !empty($register_company->settings->client_portal_privacy_policy))
<input type="checkbox" name="terms" class="form-checkbox mr-2 cursor-pointer" checked>
@ -129,7 +152,9 @@
</span>
</span>
<button class="button button-primary bg-blue-600" :disabled={{ $submitsForm == 'true' ? 'isSubmitted' : 'busy'}} x-on:click="busy = true">{{ ctrans('texts.register')}}</button>
<button class="button button-primary bg-blue-600" :disabled={{ $submitsForm == 'true' ? 'isSubmitted' : 'busy'}} x-on:click="busy = true">
{{ ctrans('texts.register')}}
</button>
</div>
</form>

View File

@ -1,4 +1,4 @@
<div class="px-4 py-5 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ $title }}
</dt>

View File

@ -196,6 +196,13 @@
</li>
@endforeach
@endif
@if(auth()->guard('contact')->check())
<li class="flex py-6">
<div class="flex w-full text-left mt-8">
<a href="{{route('client.dashboard')}}" class="button-link text-primary">{{ ctrans('texts.go_back') }}</a>
</div>
</li>
@endif
</ul>
</div>
</div>
@ -209,7 +216,7 @@
@foreach($bundle->toArray() as $item)
<div class="flex justify-between mt-1 mb-1">
<span class="font-light text-sm uppercase">{{$item['product']}} x {{$item['qty']}}</span>
<span class="font-light text-sm">{{ $item['qty'] }} x {{ substr(str_replace(["\r","\n","<BR>","<BR />","<br>","<br />"]," ", $item['product']), 0, 30) . "..." }}</span>
<span class="font-bold text-sm">{{ $item['price'] }}</span>
</div>
@endforeach
@ -284,6 +291,7 @@
</form>
@endif
@if($is_eligible)
<div class="mt-4 container mx-auto flex w-full justify-center" x-show.important="toggle" x-transition>
<span class="">
<svg class="animate-spin h-8 w-8 text-primary mx-auto justify-center w-full" xmlns="http://www.w3.org/2000/svg"
@ -295,6 +303,9 @@
</svg>
</span>
</div>
@else
<small class="mt-4 block">{{ $this->not_eligible_message }}</small>
@endif
</div>

View File

@ -13,6 +13,6 @@
@section('body')
<div class="flex flex-col">
@livewire('credits-table', ['company' => $company])
@livewire('credits-table', ['company_id' => $company->id, 'db' => $company->db])
</div>
@endsection

View File

@ -35,7 +35,6 @@
@include('portal.ninja2020.components.pdf-viewer', ['entity' => $credit, 'invitation' => $invitation])
@endsection
@section('footer')
@ -45,18 +44,5 @@
var clipboard = new ClipboardJS('.btn');
// clipboard.on('success', function(e) {
// console.info('Action:', e.action);
// console.info('Text:', e.text);
// console.info('Trigger:', e.trigger);
// e.clearSelection();
// });
// clipboard.on('error', function(e) {
// console.error('Action:', e.action);
// console.error('Trigger:', e.trigger);
// });
</script>
@endsection

View File

@ -14,5 +14,5 @@
@csrf
</form>
@livewire('documents-table', ['client' => $client, 'company' => $company])
@livewire('documents-table', ['client_id' => $client->id, 'db' => $company->db])
@endsection

View File

@ -5,6 +5,7 @@
<form action="{{route('client.payments.credit_response')}}" method="post" id="credit-payment">
@csrf
<input type="hidden" name="payment_hash" value="{{$payment_hash}}">
<input type="hidden" name="hash" value="{{ request()->query('hash')}}">
</form>
<div class="container mx-auto">

View File

@ -0,0 +1,31 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.error'))
@section('body')
<div class="flex h-screen">
<div class="m-auto md:w-1/2 lg:w-1/2">
<div class="flex flex-col items-center">
@if($account && !$account->isPaid())
<div>
<img src="{{ asset('images/invoiceninja-black-logo-2.png') }}"
class="border-b border-gray-100 h-18 pb-4" alt="Invoice Ninja logo">
</div>
@elseif(isset($company) && !is_null($company))
<div>
<img src="{{ $company->present()->logo() }}"
class="mx-auto border-b border-gray-100 h-18 pb-4" alt="{{ $company->present()->name() }} logo">
</div>
@endif
<h1 class="text-center text-3xl mt-10">{{ ctrans("texts.subscription_blocked_title") }}</h1>
<p class="text-center opacity-75 mt-10">{{ ctrans('texts.subscription_blocked') }}</p>
</div>
</div>
</div>
@stop
@push('footer')
@endpush

View File

@ -23,6 +23,6 @@
</form>
</div>
<div class="flex flex-col mt-4">
@livewire('invoices-table', ['company' => $company])
@livewire('invoices-table', ['company_id' => $company->id, 'db' => $company->db])
</div>
@endsection

View File

@ -31,7 +31,7 @@
<input type="hidden" name="company_gateway_id" id="company_gateway_id">
<input type="hidden" name="payment_method_id" id="payment_method_id">
<input type="hidden" name="signature">
<input type="hidden" name="hash" value="{{ $hash }}">
<input type="hidden" name="payable_invoices[0][amount]" value="{{ $invoice->partial > 0 ? \App\Utils\Number::formatValue($invoice->partial, $invoice->client->currency()) : \App\Utils\Number::formatValue($invoice->balance, $invoice->client->currency()) }}">
<input type="hidden" name="payable_invoices[0][invoice_id]" value="{{ $invoice->hashed_id }}">

View File

@ -3,6 +3,6 @@
@section('body')
<div class="flex flex-col">
@livewire('payment-methods-table', ['client' => $client, 'company' => $company])
@livewire('payment-methods-table', ['client_id' => $client->id, 'db' => $company->db])
</div>
@endsection

View File

@ -3,6 +3,6 @@
@section('body')
<div class="flex flex-col">
@livewire('payments-table', ['company' => $company])
@livewire('payments-table', ['company_id' => $company->id, 'db' => $company->db])
</div>
@endsection

View File

@ -26,6 +26,6 @@
</div>
<div class="flex flex-col mt-4">
@livewire('quotes-table', ['company' => $company])
@livewire('quotes-table', ['company_id' => $company->id, 'db' => $company->db])
</div>
@endsection

View File

@ -54,7 +54,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie
Route::post('invoices/payment', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'bulk'])->name('invoices.bulk');
Route::get('invoices/payment', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'catch_bulk'])->name('invoices.catch_bulk');
Route::post('invoices/download', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'download'])->name('invoices.download');
Route::get('invoices/{invoice}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show');
Route::get('invoices/{invoice}/{hash?}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show');
Route::get('invoices/{invoice_invitation}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'show'])->name('invoice.show_invitation');
Route::get('recurring_invoices', [App\Http\Controllers\ClientPortal\RecurringInvoiceController::class, 'index'])->name('recurring_invoices.index')->middleware('portal_enabled');

View File

@ -100,7 +100,7 @@ class CreditsTest extends TestCase
$c2->load('client');
$c3->load('client');
Livewire::test(CreditsTable::class, ['company' => $company])
Livewire::test(CreditsTable::class, ['company_id' => $company->id, 'db' => $company->db])
->assertDontSee('testing-number-01')
->assertSee('testing-number-02')
->assertSee('testing-number-03');
@ -167,7 +167,7 @@ class CreditsTest extends TestCase
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(CreditsTable::class, ['company' => $company])
Livewire::test(CreditsTable::class, ['company_id' => $company->id, 'db' => $company->db])
->assertSee('testing-number-01')
->assertSee('testing-number-02')
->assertSee('testing-number-03');

View File

@ -86,12 +86,12 @@ class InvoicesTest extends TestCase
$this->actingAs($client->contacts->first(), 'contact');
Livewire::test(InvoicesTable::class, ['company' => $company])
Livewire::test(InvoicesTable::class, ['company_id' => $company->id, 'db' => $company->db])
->assertSee($sent->number)
->assertSee($paid->number)
->assertSee($unpaid->number);
Livewire::test(InvoicesTable::class, ['company' => $company])
Livewire::test(InvoicesTable::class, ['company_id' => $company->id, 'db' => $company->db])
->set('status', ['paid'])
->assertSee($paid->number)
->assertDontSee($unpaid->number);

View File

@ -144,7 +144,7 @@ class ProfitAndLossReportTest extends TestCase
'balance' => 11,
'status_id' => 2,
'total_taxes' => 1,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'terms' => 'nada',
'discount' => 0,
'tax_rate1' => 0,
@ -183,7 +183,7 @@ class ProfitAndLossReportTest extends TestCase
'balance' => 10,
'status_id' => 2,
'total_taxes' => 1,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'terms' => 'nada',
'discount' => 0,
'tax_rate1' => 10,
@ -226,7 +226,7 @@ class ProfitAndLossReportTest extends TestCase
'balance' => 10,
'status_id' => 2,
'total_taxes' => 1,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'terms' => 'nada',
'discount' => 0,
'tax_rate1' => 10,
@ -282,7 +282,7 @@ class ProfitAndLossReportTest extends TestCase
'balance' => 10,
'status_id' => 2,
'total_taxes' => 0,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'terms' => 'nada',
'discount' => 0,
'tax_rate1' => 0,
@ -313,7 +313,7 @@ class ProfitAndLossReportTest extends TestCase
'amount' => 10,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
]);
$pl = new ProfitLoss($this->company, $this->payload);
@ -334,7 +334,7 @@ class ProfitAndLossReportTest extends TestCase
$e = ExpenseFactory::create($this->company->id, $this->user->id);
$e->amount = 10;
$e->date = '2022-01-01';
$e->date = now()->format('Y-m-d');
$e->calculate_tax_by_amount = true;
$e->tax_amount1 = 10;
$e->save();
@ -358,7 +358,7 @@ class ProfitAndLossReportTest extends TestCase
$e = ExpenseFactory::create($this->company->id, $this->user->id);
$e->amount = 10;
$e->date = '2022-01-01';
$e->date = now()->format('Y-m-d');
$e->tax_rate1 = 10;
$e->tax_name1 = 'GST';
$e->uses_inclusive_taxes = false;
@ -383,7 +383,7 @@ class ProfitAndLossReportTest extends TestCase
$e = ExpenseFactory::create($this->company->id, $this->user->id);
$e->amount = 10;
$e->date = '2022-01-01';
$e->date = now()->format('Y-m-d');
$e->tax_rate1 = 10;
$e->tax_name1 = 'GST';
$e->uses_inclusive_taxes = false;
@ -410,7 +410,7 @@ class ProfitAndLossReportTest extends TestCase
'amount' => 10,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'exchange_rate' => 1,
'currency_id' => $this->company->settings->currency_id,
]);
@ -440,7 +440,7 @@ class ProfitAndLossReportTest extends TestCase
'amount' => 10,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'exchange_rate' => 1,
'currency_id' => $this->company->settings->currency_id,
]);
@ -454,7 +454,7 @@ class ProfitAndLossReportTest extends TestCase
'amount' => 10,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'exchange_rate' => 1,
'currency_id' => $this->company->settings->currency_id,
]);
@ -489,7 +489,7 @@ class ProfitAndLossReportTest extends TestCase
'balance' => 10,
'status_id' => 2,
'total_taxes' => 1,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'terms' => 'nada',
'discount' => 0,
'tax_rate1' => 10,
@ -510,7 +510,7 @@ class ProfitAndLossReportTest extends TestCase
'amount' => 10,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'exchange_rate' => 1,
'currency_id' => $this->company->settings->currency_id,
]);
@ -524,7 +524,7 @@ class ProfitAndLossReportTest extends TestCase
'amount' => 10,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'date' => '2022-01-01',
'date' => now()->format('Y-m-d'),
'exchange_rate' => 1,
'currency_id' => $this->company->settings->currency_id,
]);

View File

@ -70,4 +70,14 @@ class DatesTest extends TestCase
$this->assertFalse($date_in_future->gt(Carbon::parse($date_in_past)->addDays(14)));
}
/*Test time travelling behaves as expected */
// public function testTimezoneShifts()
// {
// $this->travel(Carbon::parse('2022-12-20'));
// $this->assertEquals('2022-12-20', now()->setTimeZone('Pacific/Midway')->format('Y-m-d'));
// $this->travelBack();
// }
}

View File

@ -115,8 +115,8 @@ class GeneratesConvertedQuoteCounterTest extends TestCase
$this->assertNotNull($invoice);
$this->assertEquals('2022-Q0001', $quote->number);
$this->assertEquals('2022-I0001', $invoice->number);
$this->assertEquals(now()->format('Y'). '-Q0001', $quote->number);
$this->assertEquals(now()->format('Y'). '-I0001', $invoice->number);
$settings = $this->client->getMergedSettings();
$settings->invoice_number_counter = 100;