1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 21:22:58 +01:00

Merge pull request #6525 from turbo124/v5-develop

Fixes for auto-bill
This commit is contained in:
David Bomba 2021-08-31 18:22:35 +10:00 committed by GitHub
commit 8b399b3f64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 376 additions and 65 deletions

View File

@ -16,6 +16,9 @@ use App\Events\Invoice\InvoiceWasViewed;
use App\Events\Misc\InvitationWasViewed;
use App\Events\Quote\QuoteWasViewed;
use App\Http\Controllers\Controller;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Payment;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
@ -113,4 +116,18 @@ class InvitationController extends Controller
public function routerForIframe(string $entity, string $client_hash, string $invitation_key)
{
}
public function paymentRouter(string $contact_key, string $payment_id)
{
$contact = ClientContact::where('contact_key', $contact_key)->firstOrFail();
$payment = Payment::find($this->decodePrimaryKey($payment_id));
if($payment->client_id != $contact->client_id)
abort(403, 'You are not authorized to view this resource');
auth()->guard('contact')->login($contact, true);
return redirect()->route('client.payments.show', $payment->hashed_id);
}
}

View File

@ -69,9 +69,13 @@ class CompanyController extends BaseController
*/
public function __construct(CompanyRepository $company_repo)
{
parent::__construct();
$this->company_repo = $company_repo;
$this->middleware('password_protected')->only(['destroy']);
}
/**
@ -477,7 +481,7 @@ class CompanyController extends BaseController
*/
public function destroy(DestroyCompanyRequest $request, Company $company)
{
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);

View File

@ -127,12 +127,11 @@ class EmailController extends BaseController
$entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
$entity_obj->service()->markSent()->save();
EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data);
// ->delay(now()->addSeconds(45));
}

View File

@ -66,7 +66,8 @@ class ImportJsonController extends BaseController
$file_location = $request->file('files')
->storeAs(
'migrations',
$request->file('files')->getClientOriginalName()
$request->file('files')->getClientOriginalName(),
config('filesystems.default'),
);
if(Ninja::isHosted())

View File

@ -25,6 +25,7 @@ use App\Utils\Ninja;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\App;
class MigrationController extends BaseController
{
@ -263,6 +264,10 @@ class MigrationController extends BaseController
// Look for possible existing company (based on company keys).
$existing_company = Company::whereRaw('BINARY `company_key` = ?', [$company->company_key])->first();
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($user->account->companies()->first()->settings));
if(!$existing_company && $company_count >=10) {
$nmo = new NinjaMailerObject;

View File

@ -213,7 +213,7 @@ class PostMarkController extends BaseController
$request->input('MessageID')
);
LightLogs::create($bounce)->batch();
LightLogs::create($spam)->batch();
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company);
}

View File

@ -21,7 +21,7 @@ class UpdateAutoBilling extends Component
public function updateAutoBilling(): void
{
if ($this->invoice->auto_bill === 'optin' || $this->invoice->auto_bill === 'optout') {
if ($this->invoice->auto_bill == 'optin' || $this->invoice->auto_bill == 'optout') {
$this->invoice->auto_bill_enabled = !$this->invoice->auto_bill_enabled;
$this->invoice->save();
}

View File

@ -52,7 +52,8 @@ class PasswordProtection
$x_api_password = base64_decode($request->header('X-API-PASSWORD-BASE64'));
}
if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in')) {
// If no password supplied - then we just check if their authentication is in cache //
if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in') && !$x_api_password) {
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);

View File

@ -101,8 +101,8 @@ class UpdateRecurringInvoiceRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}
if (isset($input['auto_bill'])) {
$input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']);
if (array_key_exists('auto_bill', $input) && isset($input['auto_bill']) && $this->setAutoBillFlag($input['auto_bill'])) {
$input['auto_bill_enabled'] = true;
}
if (array_key_exists('documents', $input)) {
@ -123,13 +123,8 @@ class UpdateRecurringInvoiceRequest extends Request
*/
private function setAutoBillFlag($auto_bill) :bool
{
if ($auto_bill == 'always') {
if ($auto_bill == 'always')
return true;
}
// if($auto_bill == '')
// off / optin / optout will reset the status of this field to off to allow
// the client to choose whether to auto_bill or not.
return false;
}

View File

@ -146,7 +146,7 @@ class BaseTransformer
$number = 0;
}
return Number::parseStringFloat($number);
return Number::parseFloat($number);
}
/**

View File

@ -25,7 +25,7 @@ class ExpenseTransformer extends BaseTransformer {
'date' => isset( $data['expense.date'] ) ? date( 'Y-m-d', strtotime( $data['expense.date'] ) ) : null,
'public_notes' => $this->getString( $data, 'expense.public_notes' ),
'private_notes' => $this->getString( $data, 'expense.private_notes' ),
'expense_category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null,
'category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null,
'project_id' => isset( $data['expense.project'] ) ? $this->getProjectId( $data['expense.project'] ) : null,
'payment_type_id' => isset( $data['expense.payment_type'] ) ? $this->getPaymentTypeId( $data['expense.payment_type'] ) : null,
'payment_date' => isset( $data['expense.payment_date'] ) ? date( 'Y-m-d', strtotime( $data['expense.payment_date'] ) ) : null,

View File

@ -498,6 +498,7 @@ class CompanyExport implements ShouldQueue
if(Ninja::isHosted()) {
Storage::disk(config('filesystems.default'))->put('backups/'.$file_name, file_get_contents($zip_path));
unlink($zip_path);
}
App::forgetInstance('translator');

View File

@ -224,7 +224,7 @@ class CompanyImport implements ShouldQueue
// if(mime_content_type(Storage::path($this->file_location)) == 'text/plain')
// return Storage::path($this->file_location);
$path = TempFile::filePath(Storage::get($this->file_location), basename($this->file_location));
$path = TempFile::filePath(Storage::disk(config('filesystems.default'))->get($this->file_location), basename($this->file_location));
$zip = new ZipArchive();
$archive = $zip->open($path);
@ -235,7 +235,7 @@ class CompanyImport implements ShouldQueue
$zip->close();
$file_location = "{$file_path}/backup.json";
if (! file_exists($file_location))
if (! file_exists($file_path))
throw new NonExistingMigrationFile('Backup file does not exist, or is corrupted.');
return $file_location;
@ -568,7 +568,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(GroupSetting::class,
['user_id', 'company_id', 'id', 'hashed_id',],
['user_id', 'company_id', 'id', 'hashed_id'],
[['users' => 'user_id']],
'group_settings',
'name');
@ -580,7 +580,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(Subscription::class,
['user_id', 'assigned_user_id', 'company_id', 'id', 'hashed_id',],
['user_id', 'assigned_user_id', 'company_id', 'id', 'hashed_id'],
[['group_settings' => 'group_id'], ['users' => 'user_id'], ['users' => 'assigned_user_id']],
'subscriptions',
'name');

View File

@ -224,7 +224,7 @@ class NinjaMailerJob implements ShouldQueue
return true;
/* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */
if(Ninja::isHosted() && strpos($this->nmo->to_user->email, '@example.com') !== false)
if(Ninja::isHosted() && $this->nmo->to_user && strpos($this->nmo->to_user->email, '@example.com') !== false)
return true;
/* GMail users are uncapped */

View File

@ -55,8 +55,8 @@ class QuoteWorkflowSettings implements ShouldQueue
});
}
if ($this->client->getSetting('auto_archive_quote')) {
$this->base_repository->archive($this->quote);
}
// if ($this->client->getSetting('auto_archive_quote')) {
// $this->base_repository->archive($this->quote);
// }
}
}

View File

@ -116,7 +116,7 @@ class SendRecurring implements ShouldQueue
nlog("Invoice {$invoice->number} created");
$invoice->invitations->each(function ($invitation) use ($invoice) {
if ($invitation->contact && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) {
if ($invitation->contact && !$invitation->contact->trashed() && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) {
try{
EmailEntity::dispatch($invitation, $invoice->company);

View File

@ -262,8 +262,6 @@ class Import implements ShouldQueue
/*After a migration first some basic jobs to ensure the system is up to date*/
VersionCheck::dispatch();
// CreateCompanyPaymentTerms::dispatchNow($sp035a66, $spaa9f78);
// CreateCompanyTaskStatuses::dispatchNow($this->company, $this->user);

View File

@ -63,7 +63,7 @@ class SendFailedEmails implements ShouldQueue
$invitation = $job_meta_array['entity_name']::where('key', $job_meta_array['invitation_key'])->with('contact')->first();
if ($invitation->invoice) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $job_meta_array['reminder_template']);
}
}

View File

@ -29,6 +29,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use ZipArchive;
use Illuminate\Support\Facades\App;
class StartMigration implements ShouldQueue
{
@ -122,6 +123,10 @@ class StartMigration implements ShouldQueue
$this->company->update_products = $update_product_flag;
$this->company->save();
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
} catch (NonExistingMigrationFile | ProcessingMigrationArchiveFailed | ResourceNotAvailableForMigration | MigrationValidatorFailed | ResourceDependencyMissing | \Exception $e) {
$this->company->update_products = $update_product_flag;

View File

@ -55,6 +55,10 @@ class SystemLogger implements ShouldQueue
MultiDB::setDb($this->company->db);
$client_id = $this->client ? $this->client->id : null;
if(!$this->client && !$this->company->owner())
return;
$user_id = $this->client ? $this->client->user_id : $this->company->owner()->id;
$sl = [

View File

@ -24,6 +24,7 @@ use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
class SendVerificationNotification implements ShouldQueue
{
@ -53,6 +54,10 @@ class SendVerificationNotification implements ShouldQueue
$event->user->service()->invite($event->company);
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($event->company->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new UserAdded($event->company, $event->creating_user, $event->user);
$nmo->company = $event->company;

View File

@ -132,7 +132,6 @@ class InvoiceEmailEngine extends BaseEmailEngine
}
}
return $this;

View File

@ -45,8 +45,8 @@ class SupportMessageSent extends Mailable
$log_file->seek(PHP_INT_MAX);
$last_line = $log_file->key();
$lines = new LimitIterator($log_file, $last_line - 100, $last_line);
$log_lines = iterator_to_array($lines);
}
@ -76,6 +76,7 @@ class SupportMessageSent extends Mailable
'system_info' => $system_info,
'laravel_log' => $log_lines,
'logo' => $company->present()->logo(),
'settings' => $company->settings
]);
}
}

View File

@ -11,6 +11,8 @@
namespace App\Mail;
use App\Jobs\Invoice\CreateUbl;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\User;
@ -116,6 +118,13 @@ class TemplateEmail extends Mailable
}
if($this->invitation->invoice && $settings->ubl_email_attachment && $this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)){
$ubl_string = CreateUbl::dispatchNow($this->invitation->invoice);
$this->attachData($ubl_string, $this->invitation->invoice->getFileName('xml'));
}
return $this;
}
}

View File

@ -34,6 +34,15 @@ class GroupSetting extends StaticModel
'settings',
];
protected $appends = [
'hashed_id',
];
public function getHashedIdAttribute()
{
return $this->encodePrimaryKey($this->id);
}
protected $touches = [];
public function company()

View File

@ -84,10 +84,6 @@ class Invoice extends BaseModel
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
// 'custom_surcharge_tax1',
// 'custom_surcharge_tax2',
// 'custom_surcharge_tax3',
// 'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',

View File

@ -287,8 +287,24 @@ class Payment extends BaseModel
event(new PaymentWasVoided($this, $this->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
}
public function getLink()
// public function getLink()
// {
// return route('client.payments.show', $this->hashed_id);
// }
public function getLink() :string
{
return route('client.payments.show', $this->hashed_id);
if(Ninja::isHosted()){
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
}
else
$domain = config('ninja.app_url');
return $domain.'/client/payment/'. $this->client->contacts()->first()->contact_key .'/' .$this->hashed_id;
}
}

View File

@ -389,6 +389,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->each(function ($invoice) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
$invoice->service()->deletePdf();
});
@ -455,7 +456,7 @@ class BaseDriver extends AbstractPaymentDriver
$invoices->first()->invitations->each(function ($invitation) use ($nmo){
if ($invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
$nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo);

View File

@ -271,4 +271,68 @@ class ACH
$this->wepay_payment_driver->storeGatewayToken($data);
}
public function tokenBilling($token, $payment_hash)
{
$token_meta = $token->meta;
if(!property_exists($token_meta, 'state') || $token_meta->state != "authorized")
return redirect()->route('client.payment_methods.verification', ['payment_method' => $token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
$amount = array_sum(array_column($this->wepay_payment_driver->payment_hash->invoices(), 'amount')) + $this->wepay_payment_driver->payment_hash->fee_total;
$app_fee = (config('ninja.wepay.fee_cc_multiplier') * $amount) + config('ninja.wepay.fee_fixed');
$response = $this->wepay_payment_driver->wepay->request('checkout/create', array(
'unique_id' => Str::random(40),
'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'),
'amount' => $amount,
'currency' => $this->wepay_payment_driver->client->getCurrencyCode(),
'short_description' => 'Goods and Services',
'type' => 'goods',
'fee' => [
'fee_payer' => config('ninja.wepay.fee_payer'),
'app_fee' => $app_fee,
],
'payment_method' => array(
'type' => 'payment_bank',
'payment_bank' => array(
'id' => $token->token
)
)
));
/* Merge all data and store in the payment hash*/
$state = [
'server_response' => $response,
'payment_hash' => $this->wepay_payment_driver->payment_hash,
];
$this->wepay_payment_driver->payment_hash->data = array_merge((array) $this->wepay_payment_driver->payment_hash->data, $state);
$this->wepay_payment_driver->payment_hash->save();
if(in_array($response->state, ['authorized', 'captured'])){
//success
nlog("success");
$payment_status = $response->state == 'authorized' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING;
return $this->processSuccessfulPayment($response, $payment_status, GatewayType::BANK_TRANSFER, true);
}
if(in_array($response->state, ['released', 'cancelled', 'failed', 'expired'])){
//some type of failure
nlog("failure");
$payment_status = $response->state == 'cancelled' ? Payment::STATUS_CANCELLED : Payment::STATUS_FAILED;
$this->processUnSuccessfulPayment($response, $payment_status);
}
}
}

View File

@ -261,7 +261,8 @@ https://developer.wepay.com/api/api-calls/checkout
private function storePaymentMethod($response, $payment_method_id)
{
nlog("storing card");
nlog("storing card");
$payment_meta = new \stdClass;
$payment_meta->exp_month = (string) $response->expiration_month;
$payment_meta->exp_year = (string) $response->expiration_year;
@ -281,5 +282,60 @@ nlog("storing card");
public function tokenBilling($cgt, $payment_hash)
{
$amount = array_sum(array_column($this->wepay_payment_driver->payment_hash->invoices(), 'amount')) + $this->wepay_payment_driver->payment_hash->fee_total;
$app_fee = (config('ninja.wepay.fee_cc_multiplier') * $amount) + config('ninja.wepay.fee_fixed');
// charge the credit card
$response = $this->wepay_payment_driver->wepay->request('checkout/create', array(
'unique_id' => Str::random(40),
'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'),
'amount' => $amount,
'currency' => $this->wepay_payment_driver->client->getCurrencyCode(),
'short_description' => 'Goods and services',
'type' => 'goods',
'fee' => [
'fee_payer' => config('ninja.wepay.fee_payer'),
'app_fee' => $app_fee,
],
'payment_method' => array(
'type' => 'credit_card',
'credit_card' => array(
'id' => $cgt->token
)
)
));
/* Merge all data and store in the payment hash*/
$state = [
'server_response' => $response,
'payment_hash' => $payment_hash,
];
$this->wepay_payment_driver->payment_hash->data = array_merge((array) $this->wepay_payment_driver->payment_hash->data, $state);
$this->wepay_payment_driver->payment_hash->save();
if(in_array($response->state, ['authorized', 'captured'])){
//success
nlog("success");
$payment_status = $response->state == 'authorized' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING;
return $this->processSuccessfulPayment($response, $payment_status, GatewayType::CREDIT_CARD, true);
}
if(in_array($response->state, ['released', 'cancelled', 'failed', 'expired'])){
//some type of failure
nlog("failure");
$payment_status = $response->state == 'cancelled' ? Payment::STATUS_CANCELLED : Payment::STATUS_FAILED;
$this->processUnSuccessfulPayment($response, $payment_status);
}
}
}

View File

@ -22,7 +22,7 @@ trait WePayCommon
{
private function processSuccessfulPayment($response, $payment_status, $gateway_type)
private function processSuccessfulPayment($response, $payment_status, $gateway_type, $return_payment = false)
{
if($gateway_type == GatewayType::BANK_TRANSFER)
@ -48,6 +48,9 @@ trait WePayCommon
$this->wepay_payment_driver->client->company,
);
if($return_payment)
return $payment;
return redirect()->route('client.payments.show', ['payment' => $this->wepay_payment_driver->encodePrimaryKey($payment->id)]);
}

View File

@ -119,14 +119,14 @@ class WePayPaymentDriver extends BaseDriver
$contact = $client->primary_contact()->first() ? $client->primary_contact()->first() : $lient->contacts->first();
$data['contact'] = $contact;
return $this->payment_method->authorizeView($data); //this is your custom implementation from here
return $this->payment_method->authorizeView($data);
}
public function authorizeResponse($request)
{
$this->init();
return $this->payment_method->authorizeResponse($request); //this is your custom implementation from here
return $this->payment_method->authorizeResponse($request);
}
public function verificationView(ClientGatewayToken $cgt)
@ -147,19 +147,23 @@ class WePayPaymentDriver extends BaseDriver
{
$this->init();
return $this->payment_method->paymentView($data); //this is your custom implementation from here
return $this->payment_method->paymentView($data);
}
public function processPaymentResponse($request)
{
$this->init();
return $this->payment_method->paymentResponse($request); //this is your custom implementation from here
return $this->payment_method->paymentResponse($request);
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
return $this->payment_method->yourTokenBillingImplmentation(); //this is your custom implementation from here
$this->init();
$this->setPaymentMethod($cgt->gateway_type_id);
$this->setPaymentHash($payment_hash);
return $this->payment_method->tokenBilling($cgt, $payment_hash);
}
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)

View File

@ -44,7 +44,7 @@ class SendEmail
}
$this->credit->invitations->each(function ($invitation) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
$email_builder = (new CreditEmail())->build($invitation, $this->reminder_template);
// EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company);

View File

@ -192,6 +192,8 @@ class InvoiceService
public function handleCancellation()
{
$this->removeUnpaidGatewayFees();
$this->invoice = (new HandleCancellation($this->invoice))->run();
return $this;
@ -199,6 +201,8 @@ class InvoiceService
public function markDeleted()
{
$this->removeUnpaidGatewayFees();
$this->invoice = (new MarkInvoiceDeleted($this->invoice))->run();
return $this;
@ -213,6 +217,8 @@ class InvoiceService
public function reverseCancellation()
{
$this->removeUnpaidGatewayFees();
$this->invoice = (new HandleCancellation($this->invoice))->reverse();
return $this;
@ -278,11 +284,14 @@ class InvoiceService
public function updateStatus()
{
if ((int)$this->invoice->balance == 0) {
if($this->invoice->status_id == Invoice::STATUS_DRAFT)
return $this;
// if ((int)$this->invoice->balance == 0) {
$this->setStatus(Invoice::STATUS_PAID)->workFlow();
// InvoiceWorkflowSettings::dispatchNow($this->invoice);
}
// $this->setStatus(Invoice::STATUS_PAID)->workFlow();
// }
if ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
$this->setStatus(Invoice::STATUS_PARTIAL);

View File

@ -26,6 +26,8 @@ class MarkInvoiceDeleted extends AbstractService
private $total_payments = 0;
private $balance_adjustment = 0;
public function __construct(Invoice $invoice)
{
$this->invoice = $invoice;
@ -51,7 +53,7 @@ class MarkInvoiceDeleted extends AbstractService
private function adjustLedger()
{
// $this->invoice->ledger()->updatePaymentBalance($this->adjustment_amount * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals
$this->invoice->ledger()->updatePaymentBalance($this->invoice->balance * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals
$this->invoice->ledger()->updatePaymentBalance($this->balance_adjustment * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals
return $this;
}
@ -65,7 +67,7 @@ class MarkInvoiceDeleted extends AbstractService
private function adjustBalance()
{
$this->invoice->client->service()->updateBalance($this->invoice->balance * -1)->save(); //reduces the client balance by the invoice amount.
$this->invoice->client->service()->updateBalance($this->balance_adjustment * -1)->save(); //reduces the client balance by the invoice amount.
return $this;
}
@ -122,11 +124,14 @@ class MarkInvoiceDeleted extends AbstractService
}
$this->total_payments = $this->invoice->payments->sum('amount') - $this->invoice->payments->sum('refunded');;
$this->total_payments = $this->invoice->payments->sum('amount') - $this->invoice->payments->sum('refunded');
$this->balance_adjustment = $this->invoice->balance;
//$this->total_payments = $this->invoice->payments->sum('amount - refunded');
nlog("adjustment amount = {$this->adjustment_amount}");
nlog("total payments = {$this->total_payments}");
// nlog("adjustment amount = {$this->adjustment_amount}");
// nlog("total payments = {$this->total_payments}");
return $this;
}

View File

@ -44,7 +44,7 @@ class SendEmail extends AbstractService
}
$this->invoice->invitations->each(function ($invitation) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
}
});

View File

@ -81,8 +81,14 @@ class RefundPayment
if ($response['success'] == false) {
$this->payment->save();
throw new PaymentRefundFailed($response['description']);
if(array_key_exists('description', $response))
throw new PaymentRefundFailed($response['description']);
else
throw new PaymentRefundFailed();
}
}
} else {
$this->payment->refunded += $this->total_refund;

View File

@ -42,7 +42,7 @@ class SendEmail
}
$this->quote->invitations->each(function ($invitation) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template);
}
});

View File

@ -47,8 +47,9 @@ trait Inviteable
{
$entity_type = Str::snake(class_basename($this->entityType()));
if(Ninja::isHosted())
if(Ninja::isHosted()){
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
}
else
$domain = config('ninja.app_url');

View File

@ -145,4 +145,4 @@
},
"minimum-stability": "dev",
"prefer-stable": true
}
}

View File

@ -372,9 +372,12 @@ CREATE TABLE `companies` (
`expense_inclusive_taxes` tinyint(1) NOT NULL DEFAULT '0',
`session_timeout` int(11) NOT NULL DEFAULT '0',
`oauth_password_required` tinyint(1) NOT NULL DEFAULT '0',
`invoice_task_datelog` tinyint(1) NOT NULL DEFAULT '0',
`invoice_task_datelog` tinyint(1) NOT NULL DEFAULT '1',
`default_password_timeout` int(11) NOT NULL DEFAULT '30',
`show_task_end_date` tinyint(1) NOT NULL DEFAULT '0',
`markdown_enabled` tinyint(1) NOT NULL DEFAULT '1',
`use_comma_as_decimal_place` tinyint(1) NOT NULL DEFAULT '0',
`report_include_drafts` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `companies_company_key_unique` (`company_key`),
KEY `companies_industry_id_foreign` (`industry_id`),
@ -1959,3 +1962,14 @@ INSERT INTO `migrations` VALUES (80,'2021_06_24_095942_payments_table_currency_n
INSERT INTO `migrations` VALUES (81,'2021_06_24_115919_update_designs',2);
INSERT INTO `migrations` VALUES (82,'2021_07_08_115919_update_designs',3);
INSERT INTO `migrations` VALUES (83,'2021_07_10_085821_activate_payfast_payment_driver',3);
INSERT INTO `migrations` VALUES (84,'2021_07_19_074503_set_invoice_task_datelog_true_in_companies_table',4);
INSERT INTO `migrations` VALUES (85,'2021_07_20_095537_activate_paytrace_payment_driver',4);
INSERT INTO `migrations` VALUES (86,'2021_07_21_213344_change_english_languages_tables',4);
INSERT INTO `migrations` VALUES (87,'2021_07_21_234227_activate_eway_payment_driver',4);
INSERT INTO `migrations` VALUES (88,'2021_08_03_115024_activate_mollie_payment_driver',4);
INSERT INTO `migrations` VALUES (89,'2021_08_05_235942_add_zelle_payment_type',4);
INSERT INTO `migrations` VALUES (90,'2021_08_07_222435_add_markdown_enabled_column_to_companies_table',4);
INSERT INTO `migrations` VALUES (91,'2021_08_10_034407_add_more_languages',4);
INSERT INTO `migrations` VALUES (92,'2021_08_18_220124_use_comma_as_decimal_place_companies_table',4);
INSERT INTO `migrations` VALUES (93,'2021_08_24_115919_update_designs',4);
INSERT INTO `migrations` VALUES (94,'2021_08_25_093105_report_include_drafts_in_companies_table',5);

View File

@ -1,4 +1,4 @@
@component('email.template.admin', ['logo' => $logo ?? 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png'])
@component('email.template.admin', ['settings' => $settings, 'logo' => $logo ?? 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png'])
{{-- Body --}}
{{ $support_message }}

View File

@ -41,7 +41,7 @@
</span>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
<span role="button" wire:click="sortBy('type_id')" class="cursor-pointer">
<span role="button" wire:click="sortBy('gateway_type_id')" class="cursor-pointer">
{{ ctrans('texts.payment_type_id') }}
</span>
</th>

View File

@ -47,7 +47,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('companies/purge/{company}', 'MigrationController@purgeCompany')->middleware('password_protected');
Route::post('companies/purge_save_settings/{company}', 'MigrationController@purgeCompanySaveSettings')->middleware('password_protected');
Route::resource('companies', 'CompanyController'); // name = (companies. index / create / show / update / destroy / edit
Route::put('companies/{company}/upload', 'CompanyController@upload');
Route::get('company_ledger', 'CompanyLedgerController@index')->name('company_ledger.index');

View File

@ -25,6 +25,8 @@ Route::get('client/key_login/{contact_key}', 'ClientPortal\ContactHashLoginContr
Route::get('client/magic_link/{magic_link}', 'ClientPortal\ContactHashLoginController@magicLink')->name('client.contact_magic_link')->middleware(['domain_db','contact_key_login']);
Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download')->middleware(['document_db']);
Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name('client.error');
Route::get('client/payment/{contact_key}/{payment_id}', 'ClientPortal\InvitationController@paymentRouter')->middleware(['domain_db','contact_key_login']);
Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit
@ -95,6 +97,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key');
Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe')->name('invoice.client_hash_and_invitation_key'); //should never need this
});
Route::get('phantom/{entity}/{invitation_key}', '\App\Utils\PhantomJS\Phantom@displayInvitation')->middleware(['invite_db', 'phantom_secret'])->name('phantom_view');

View File

@ -54,11 +54,11 @@ class CancelInvoiceTest extends TestCase
$this->assertEquals(Invoice::STATUS_SENT, $this->invoice->status_id);
$this->invoice->service()->handleCancellation()->save();
$this->invoice->fresh()->service()->handleCancellation()->save();
$this->assertEquals(0, $this->invoice->fresh()->balance);
$this->assertEquals($this->client->fresh()->balance, ($client_balance - $invoice_balance));
$this->assertNotEquals($client_balance, $this->client->fresh()->balance);
$this->assertEquals(Invoice::STATUS_CANCELLED, $this->invoice->status_id);
$this->assertEquals(Invoice::STATUS_CANCELLED, $this->invoice->fresh()->status_id);
}
}

View File

@ -11,6 +11,7 @@
namespace Tests\Feature;
use App\DataMapper\CompanySettings;
use App\Http\Middleware\PasswordProtection;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Utils\Traits\MakesHash;
@ -47,6 +48,8 @@ class CompanyTest extends TestCase
public function testCompanyList()
{
$this->withoutMiddleware(PasswordProtection::class);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
@ -117,6 +120,7 @@ class CompanyTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
'X-API-PASSWORD' => 'ALongAndBriliantPassword',
])->delete('/api/v1/companies/'.$this->encodePrimaryKey($company->id))
->assertStatus(200);
}

View File

@ -14,6 +14,7 @@ use App\Factory\ClientFactory;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\Payment;
use App\Utils\Traits\MakesHash;
@ -63,6 +64,14 @@ class RefundTest extends TestCase
$client = ClientFactory::create($this->company->id, $this->user->id);
$client->save();
$contact = ClientContact::factory()->create([
'user_id' => $this->user->id,
'client_id' => $client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
'send_email' => true,
]);
$this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id
$this->invoice->client_id = $client->id;
$this->invoice->status_id = Invoice::STATUS_SENT;
@ -138,6 +147,15 @@ class RefundTest extends TestCase
$client = ClientFactory::create($this->company->id, $this->user->id);
$client->save();
$contact = ClientContact::factory()->create([
'user_id' => $this->user->id,
'client_id' => $client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
'send_email' => true,
]);
$this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id
$this->invoice->client_id = $client->id;
$this->invoice->status_id = Invoice::STATUS_SENT;
@ -227,6 +245,14 @@ class RefundTest extends TestCase
$client = ClientFactory::create($this->company->id, $this->user->id);
$client->save();
$contact = ClientContact::factory()->create([
'user_id' => $this->user->id,
'client_id' => $client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
'send_email' => true,
]);
$this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id
$this->invoice->client_id = $client->id;
$this->invoice->status_id = Invoice::STATUS_SENT;
@ -303,6 +329,15 @@ class RefundTest extends TestCase
$client = ClientFactory::create($this->company->id, $this->user->id);
$client->save();
$contact = ClientContact::factory()->create([
'user_id' => $this->user->id,
'client_id' => $client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
'send_email' => true,
]);
$this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id
$this->invoice->client_id = $client->id;
$this->invoice->status_id = Invoice::STATUS_SENT;
@ -388,6 +423,15 @@ class RefundTest extends TestCase
$client = ClientFactory::create($this->company->id, $this->user->id);
$client->save();
$contact = ClientContact::factory()->create([
'user_id' => $this->user->id,
'client_id' => $client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
'send_email' => true,
]);
$this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id
$this->invoice->client_id = $client->id;
$this->invoice->status_id = Invoice::STATUS_SENT;
@ -497,6 +541,15 @@ class RefundTest extends TestCase
$client = ClientFactory::create($this->company->id, $this->user->id);
$client->save();
$contact = ClientContact::factory()->create([
'user_id' => $this->user->id,
'client_id' => $client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
'send_email' => true,
]);
$this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id
$this->invoice->client_id = $client->id;
$this->invoice->status_id = Invoice::STATUS_SENT;

View File

@ -41,6 +41,27 @@ class NumberTest extends TestCase
$this->assertEquals(2.15, $rounded);
}
//this method proved an error! removing this method from production
// public function testImportFloatConversion()
// {
// $amount = '€7,99';
// $converted_amount = Number::parseStringFloat($amount);
// $this->assertEquals(799, $converted_amount);
// }
public function testParsingStringCurrency()
{
$amount = '€7,99';
$converted_amount = Number::parseFloat($amount);
$this->assertEquals(7.99, $converted_amount);
}
// public function testParsingFloats()
// {
// Currency::all()->each(function ($currency) {