Post Merge v5-develop

This commit is contained in:
David Bomba 2022-07-15 17:41:30 +10:00
commit 071f2ee102
192 changed files with 380184 additions and 350081 deletions

View File

@ -1 +1 @@
5.4.4 5.4.10

View File

@ -25,6 +25,9 @@ class CompanySettings extends BaseSettings
/*Invoice*/ /*Invoice*/
public $auto_archive_invoice = false; // @implemented public $auto_archive_invoice = false; // @implemented
public $qr_iban = ''; //@implemented
public $besr_id = ''; //@implemented
public $lock_invoices = 'off'; //off,when_sent,when_paid //@implemented public $lock_invoices = 'off'; //off,when_sent,when_paid //@implemented
public $enable_client_portal_tasks = false; //@ben to implement public $enable_client_portal_tasks = false; //@ben to implement
@ -433,7 +436,12 @@ class CompanySettings extends BaseSettings
public $auto_archive_invoice_cancelled = false; public $auto_archive_invoice_cancelled = false;
public $vendor_portal_enable_uploads=false;
public static $casts = [ public static $casts = [
'vendor_portal_enable_uploads' => 'bool',
'besr_id' => 'string',
'qr_iban' => 'string',
'email_subject_purchase_order' => 'string', 'email_subject_purchase_order' => 'string',
'email_template_purchase_order' => 'string', 'email_template_purchase_order' => 'string',
'require_purchase_order_signature' => 'bool', 'require_purchase_order_signature' => 'bool',

View File

@ -13,7 +13,8 @@ class PaymentFailed extends Exception
public function render($request) public function render($request)
{ {
if (auth()->user() || ($request->has('cko-session-id') && $request->query('cko-session-id'))) { if (auth()->guard('contact')->user() || ($request->has('cko-session-id') && $request->query('cko-session-id') )) {
return render('gateways.unsuccessful', [ return render('gateways.unsuccessful', [
'message' => $this->getMessage(), 'message' => $this->getMessage(),
'code' => $this->getCode(), 'code' => $this->getCode(),

View File

@ -11,6 +11,7 @@
namespace App\Factory; namespace App\Factory;
use App\Models\CompanyUser;
use App\Models\User; use App\Models\User;
class UserFactory class UserFactory

View File

@ -68,7 +68,7 @@ class InvoiceFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
public function number(string $number) :Builder public function number(string $number = '') :Builder
{ {
return $this->builder->where('number', $number); return $this->builder->where('number', $number);
} }
@ -138,6 +138,14 @@ class InvoiceFilters extends QueryFilters
}); });
} }
public function without_deleted_clients()
{
return $this->builder->whereHas('client', function ($query) {
$query->where('is_deleted',0);
});
}
public function upcoming() public function upcoming()
{ {
return $this->builder return $this->builder

View File

@ -94,6 +94,9 @@ class QuoteFilters extends QueryFilters
{ {
$sort_col = explode('|', $sort); $sort_col = explode('|', $sort);
if($sort_col[0] == 'valid_until')
$sort_col[0] = 'due_date';
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $sort_col[1]);
} }

View File

@ -102,16 +102,12 @@ class InvoiceSum
private function calculateCustomValues() private function calculateCustomValues()
{ {
// $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_tax1);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1);
// $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_tax2);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2);
// $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_tax3);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3);
// $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_tax4);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4);
$this->total += $this->total_custom_values; $this->total += $this->total_custom_values;
@ -155,7 +151,7 @@ class InvoiceSum
*/ */
private function calculateBalance() private function calculateBalance()
{ {
//$this->invoice->balance = $this->balance($this->getTotal(), $this->invoice);
$this->setCalculatedAttributes(); $this->setCalculatedAttributes();
return $this; return $this;
@ -174,22 +170,6 @@ class InvoiceSum
{ {
$this->total += $this->total_taxes; $this->total += $this->total_taxes;
// if (is_numeric($this->invoice->custom_value1)) {
// $this->total += $this->invoice->custom_value1;
// }
// if (is_numeric($this->invoice->custom_value2)) {
// $this->total += $this->invoice->custom_value2;
// }
// if (is_numeric($this->invoice->custom_value3)) {
// $this->total += $this->invoice->custom_value3;
// }
// if (is_numeric($this->invoice->custom_value4)) {
// $this->total += $this->invoice->custom_value4;
// }
return $this; return $this;
} }

View File

@ -0,0 +1,151 @@
<?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\Helpers\SwissQr;
use App\Models\Client;
use App\Models\Company;
use App\Models\Invoice;
use Sprain\SwissQrBill as QrBill;
/**
* SwissQrGenerator.
*/
class SwissQrGenerator
{
protected Company $company;
protected $invoice;
protected Client $client;
public function __construct($invoice, Company $company)
{
$this->company = $company;
$this->invoice = $invoice;
$this->client = $invoice->client;
}
private function calcDueAmount()
{
if($this->invoice->partial > 0)
return $this->invoice->partial;
if($this->invoice->status_id == Invoice::STATUS_DRAFT)
return $this->invoice->amount;
return $this->invoice->balance;
}
public function run()
{
// This is an example how to create a typical qr bill:
// - with reference number
// - with known debtor
// - with specified amount
// - with human-readable additional information
// - using your QR-IBAN
//
// Likely the most common use-case in the business world.
// Create a new instance of QrBill, containing default headers with fixed values
$qrBill = QrBill\QrBill::create();
// Add creditor information
// Who will receive the payment and to which bank account?
$qrBill->setCreditor(
QrBill\DataGroup\Element\CombinedAddress::create(
$this->company->present()->name(),
$this->company->present()->address1(),
$this->company->present()->getCompanyCityState(),
'CH'
));
$qrBill->setCreditorInformation(
QrBill\DataGroup\Element\CreditorInformation::create(
$this->company->present()->qr_iban() ?: '' // This is a special QR-IBAN. Classic IBANs will not be valid here.
));
// Add debtor information
// Who has to pay the invoice? This part is optional.
//
// Notice how you can use two different styles of addresses: CombinedAddress or StructuredAddress
// They are interchangeable for creditor as well as debtor.
$qrBill->setUltimateDebtor(
QrBill\DataGroup\Element\StructuredAddress::createWithStreet(
substr($this->client->present()->name(), 0 , 70),
$this->client->address1 ? substr($this->client->address1, 0 , 70) : '',
$this->client->address2 ? substr($this->client->address2, 0 , 16) : '',
$this->client->postal_code ? substr($this->client->postal_code, 0, 16) : '',
$this->client->city ? substr($this->client->postal_code, 0, 35) : '',
'CH'
));
// Add payment amount information
// What amount is to be paid?
$qrBill->setPaymentAmountInformation(
QrBill\DataGroup\Element\PaymentAmountInformation::create(
'CHF',
$this->calcDueAmount()
));
// Add payment reference
// This is what you will need to identify incoming payments.
$referenceNumber = QrBill\Reference\QrPaymentReferenceGenerator::generate(
$this->company->present()->besr_id() ?: '', // You receive this number from your bank (BESR-ID). Unless your bank is PostFinance, in that case use NULL.
$this->invoice->number// A number to match the payment with your internal data, e.g. an invoice number
);
$qrBill->setPaymentReference(
QrBill\DataGroup\Element\PaymentReference::create(
QrBill\DataGroup\Element\PaymentReference::TYPE_QR,
$referenceNumber
));
// Optionally, add some human-readable information about what the bill is for.
$qrBill->setAdditionalInformation(
QrBill\DataGroup\Element\AdditionalInformation::create(
$this->invoice->public_notes ?: ''
)
);
// Now get the QR code image and save it as a file.
try {
$output = new QrBill\PaymentPart\Output\HtmlOutput\HtmlOutput($qrBill, 'en');
$html = $output
->setPrintable(false)
->getPaymentPart();
return $html;
} catch (\Exception $e) {
foreach($qrBill->getViolations() as $key => $violation) {
nlog("qr");
nlog($violation);
}
return '';
// return $e->getMessage();
}
}
}

View File

@ -46,6 +46,7 @@ use Laravel\Socialite\Facades\Socialite;
use Microsoft\Graph\Model; use Microsoft\Graph\Model;
use PragmaRX\Google2FA\Google2FA; use PragmaRX\Google2FA\Google2FA;
use Turbo124\Beacon\Facades\LightLogs; use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Support\Facades\Http;
class LoginController extends BaseController class LoginController extends BaseController
{ {
@ -326,18 +327,14 @@ class LoginController extends BaseController
if (request()->input('provider') == 'google') { if (request()->input('provider') == 'google') {
return $this->handleGoogleOauth(); return $this->handleGoogleOauth();
} elseif (request()->input('provider') == 'microsoft') { } elseif (request()->input('provider') == 'microsoft') {
// if (request()->has('token')) {
// return $this->handleSocialiteLogin('microsoft', request()->get('token'));
// } else {
// $message = 'Bearer token missing for the microsoft login';
// }
return $this->handleMicrosoftOauth(); return $this->handleMicrosoftOauth();
} elseif (request()->input('provider') == 'apple') { } elseif (request()->input('provider') == 'apple') {
// if (request()->has('token')) { if (request()->has('id_token')) {
// return $this->handleSocialiteLogin('apple', request()->get('token')); $token = request()->input('id_token');
// } else { return $this->handleSocialiteLogin('apple', $token);
// $message = 'Token is missing for the apple login'; } else {
// } $message = 'Token is missing for the apple login';
}
} }
return response() return response()
@ -354,6 +351,7 @@ class LoginController extends BaseController
private function handleSocialiteLogin($provider, $token) private function handleSocialiteLogin($provider, $token)
{ {
$user = $this->getSocialiteUser($provider, $token); $user = $this->getSocialiteUser($provider, $token);
nlog($user);
if ($user) { if ($user) {
return $this->loginOrCreateFromSocialite($user, $provider); return $this->loginOrCreateFromSocialite($user, $provider);
} }
@ -490,9 +488,11 @@ class LoginController extends BaseController
{ {
if (request()->has('accessToken')) { if (request()->has('accessToken')) {
$accessToken = request()->input('accessToken'); $accessToken = request()->input('accessToken');
} else { elseif(request()->has('access_token'))
return response()->json(['message' => 'Invalid response from oauth server'], 400); $accessToken = request()->input('access_token');
} else
return response()->json(['message' => 'Invalid response from oauth server, no access token in response.'], 400);
$graph = new \Microsoft\Graph\Graph(); $graph = new \Microsoft\Graph\Graph();
$graph->setAccessToken($accessToken); $graph->setAccessToken($accessToken);
@ -503,6 +503,7 @@ class LoginController extends BaseController
if ($user) { if ($user) {
$account = request()->input('account'); $account = request()->input('account');
$email = $user->getMail() ?: $user->getUserPrincipalName(); $email = $user->getMail() ?: $user->getUserPrincipalName();
$query = [ $query = [
@ -541,6 +542,10 @@ class LoginController extends BaseController
return $this->createNewAccount($new_account); return $this->createNewAccount($new_account);
} }
return response()->json(['message' => 'Unable to authenticate this user'], 400);
} }
private function existingOauthUser($existing_user) private function existingOauthUser($existing_user)
@ -686,8 +691,8 @@ class LoginController extends BaseController
} }
if($provider == 'microsoft'){ if($provider == 'microsoft'){
$scopes = ['email', 'Mail.ReadWrite', 'Mail.Send', 'offline_access', 'profile', 'User.Read openid']; $scopes = ['email', 'Mail.Send', 'offline_access', 'profile', 'User.Read openid'];
$parameters = ['response_type' => 'code', 'redirect_uri' => config('ninja.app_url') . '/auth/microsoft']; $parameters = ['response_type' => 'code', 'redirect_uri' => config('ninja.app_url')."/auth/microsoft"];
} }
if (request()->has('code')) { if (request()->has('code')) {
@ -751,7 +756,10 @@ class LoginController extends BaseController
$oauth_user_token = $socialite_user->accessTokenResponseBody['access_token']; $oauth_user_token = $socialite_user->accessTokenResponseBody['access_token'];
if ($user = OAuth::handleAuth($socialite_user, $provider)) { $oauth_expiry = now()->addSeconds($socialite_user->accessTokenResponseBody['expires_in']) ?: now()->addSeconds(300);
if($user = OAuth::handleAuth($socialite_user, $provider))
{
nlog('found user and updating their user record'); nlog('found user and updating their user record');
$name = OAuth::splitName($socialite_user->getName()); $name = OAuth::splitName($socialite_user->getName());
@ -763,6 +771,7 @@ class LoginController extends BaseController
'oauth_provider_id' => $provider, 'oauth_provider_id' => $provider,
'oauth_user_token' => $oauth_user_token, 'oauth_user_token' => $oauth_user_token,
'oauth_user_refresh_token' => $socialite_user->accessTokenResponseBody['refresh_token'], 'oauth_user_refresh_token' => $socialite_user->accessTokenResponseBody['refresh_token'],
'oauth_user_token_expiry' => $oauth_expiry,
]; ];
$user->update($update_user); $user->update($update_user);

View File

@ -80,6 +80,7 @@ class BaseController extends Controller
'company.groups.documents', 'company.groups.documents',
'company.invoices.invitations.contact', 'company.invoices.invitations.contact',
'company.invoices.invitations.company', 'company.invoices.invitations.company',
'company.purchase_orders.invitations',
'company.invoices.documents', 'company.invoices.documents',
'company.products', 'company.products',
'company.products.documents', 'company.products.documents',
@ -767,6 +768,10 @@ class BaseController extends Controller
return redirect('/')->with(['login' => 'true']); return redirect('/')->with(['login' => 'true']);
} }
if (request()->has('signup') && request()->input('signup') == 'true') {
return redirect('/')->with(['signup' => 'true']);
}
$data = []; $data = [];
//pass report errors bool to front end //pass report errors bool to front end
@ -776,11 +781,16 @@ class BaseController extends Controller
$data['rc'] = request()->has('rc') ? request()->input('rc') : ''; $data['rc'] = request()->has('rc') ? request()->input('rc') : '';
$data['build'] = request()->has('build') ? request()->input('build') : ''; $data['build'] = request()->has('build') ? request()->input('build') : '';
$data['login'] = request()->has('login') ? request()->input('login') : 'false'; $data['login'] = request()->has('login') ? request()->input('login') : 'false';
$data['signup'] = request()->has('signup') ? request()->input('signup') : 'false';
if (request()->session()->has('login')) { if (request()->session()->has('login')) {
$data['login'] = 'true'; $data['login'] = 'true';
} }
if(request()->session()->has('signup')){
$data['signup'] = 'true';
}
$data['user_agent'] = request()->server('HTTP_USER_AGENT'); $data['user_agent'] = request()->server('HTTP_USER_AGENT');
$data['path'] = $this->setBuild(); $data['path'] = $this->setBuild();

View File

@ -650,4 +650,85 @@ class ClientController extends BaseController
//todo add an event here using the client name as reference for purge event //todo add an event here using the client name as reference for purge event
} }
/**
* Update the specified resource in storage.
*
* @param PurgeClientRequest $request
* @param Client $client
* @param string $mergeable client hashed_id
* @return Response
*
*
*
* @OA\Post(
* path="/api/v1/clients/{id}/{mergaeble_client_hashed_id}/merge",
* operationId="mergeClient",
* tags={"clients"},
* summary="Merges two clients",
* description="Handles merging 2 clients",
* @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\Parameter(
* name="id",
* in="path",
* description="The Client Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Parameter(
* name="mergeable_client_hashedid",
* in="path",
* description="The Mergeable Client Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the client 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\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 merge(PurgeClientRequest $request, Client $client, string $mergeable_client)
{
$m_client = Client::withTrashed()
->where('id', $this->decodePrimaryKey($mergeable_client))
->where('company_id', auth()->user()->company()->id)
->first();
if(!$m_client)
return response()->json(['message' => "Client not found"]);
$merged_client = $client->service()->merge($m_client)->save();
return $this->itemResponse($merged_client);
}
} }

View File

@ -235,6 +235,9 @@ class InvitationController extends Controller
->with('contact.client') ->with('contact.client')
->firstOrFail(); ->firstOrFail();
if($invitation->contact->trashed())
$invitation->contact->restore();
auth()->guard('contact')->loginUsingId($invitation->contact->id, true); auth()->guard('contact')->loginUsingId($invitation->contact->id, true);
$invoice = $invitation->invoice; $invoice = $invitation->invoice;

View File

@ -25,6 +25,7 @@ use App\Models\GatewayType;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Models\Subscription; use App\Models\Subscription;
use App\Notifications\Ninja\NewAccountNotification;
use App\Repositories\SubscriptionRepository; use App\Repositories\SubscriptionRepository;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -156,6 +157,9 @@ class NinjaPlanController extends Controller
->increment() ->increment()
->queue(); ->queue();
$ninja_company = Company::on('db-ninja-01')->find(config('ninja.ninja_default_company_id'));
$ninja_company->notification(new NewAccountNotification($account, $client))->ninja();
return $this->render('plan.trial_confirmed', $data); return $this->render('plan.trial_confirmed', $data);
} }

View File

@ -90,13 +90,17 @@ class PaymentController extends Controller
public function response(PaymentResponseRequest $request) public function response(PaymentResponseRequest $request)
{ {
$gateway = CompanyGateway::findOrFail($request->input('company_gateway_id')); $gateway = CompanyGateway::findOrFail($request->input('company_gateway_id'));
$payment_hash = PaymentHash::where('hash', $request->payment_hash)->first(); $payment_hash = PaymentHash::where('hash', $request->payment_hash)->firstOrFail();
$invoice = Invoice::with('client')->find($payment_hash->fee_invoice_id); $invoice = Invoice::with('client')->find($payment_hash->fee_invoice_id);
$client = $invoice ? $invoice->client : auth()->user()->client; $client = $invoice ? $invoice->client : auth()->guard('contact')->user()->client;
// 09-07-2022 catch duplicate responses for invoices that already paid here.
if($invoice && $invoice->status_id == Invoice::STATUS_PAID)
abort(400, 'Invoice paid. Duplicate submission');
return $gateway return $gateway
// ->driver(auth()->user()->client)
->driver($client) ->driver($client)
->setPaymentMethod($request->input('payment_method_id')) ->setPaymentMethod($request->input('payment_method_id'))
->setPaymentHash($payment_hash) ->setPaymentHash($payment_hash)

View File

@ -180,7 +180,7 @@ class QuoteController extends Controller
if ($process) { if ($process) {
foreach ($quotes as $quote) { foreach ($quotes as $quote) {
$quote->service()->approve(auth()->user())->save(); $quote->service()->approve(auth()->user())->save();
event(new QuoteWasApproved(auth()->guard('contact')->user(), $quote, $quote->company, Ninja::eventVars())); // event(new QuoteWasApproved(auth()->guard('contact')->user(), $quote, $quote->company, Ninja::eventVars()));
if (request()->has('signature') && ! is_null(request()->signature) && ! empty(request()->signature)) { if (request()->has('signature') && ! is_null(request()->signature) && ! empty(request()->signature)) {
InjectSignature::dispatch($quote, request()->signature); InjectSignature::dispatch($quote, request()->signature);

View File

@ -22,6 +22,7 @@ use Google_Client;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Microsoft\Graph\Model;
class ConnectedAccountController extends BaseController class ConnectedAccountController extends BaseController
{ {
@ -81,12 +82,61 @@ class ConnectedAccountController extends BaseController
return $this->handleGoogleOauth(); return $this->handleGoogleOauth();
} }
if ($request->input('provider') == 'microsoft') {
return $this->handleMicrosoftOauth($request);
}
return response() return response()
->json(['message' => 'Provider not supported'], 400) ->json(['message' => 'Provider not supported'], 400)
->header('X-App-Version', config('ninja.app_version')) ->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version')); ->header('X-Api-Version', config('ninja.minimum_client_version'));
} }
private function handleMicrosoftOauth($request)
{
nlog($request->all());
if(!$request->has('access_token'))
return response()->json(['message' => 'No access_token parameter found!'], 400);
$graph = new \Microsoft\Graph\Graph();
$graph->setAccessToken($request->input('access_token'));
$user = $graph->createRequest("GET", "/me")
->setReturnType(Model\User::class)
->execute();
if($user){
$email = $user->getMail() ?: $user->getUserPrincipalName();
if(auth()->user()->email != $email && MultiDB::checkUserEmailExists($email))
return response()->json(['message' => ctrans('texts.email_already_register')], 400);
$connected_account = [
'email' => $email,
'oauth_user_id' => $user->getId(),
'oauth_provider_id' => 'microsoft',
'email_verified_at' =>now()
];
auth()->user()->update($connected_account);
auth()->user()->email_verified_at = now();
auth()->user()->save();
$this->setLoginCache(auth()->user());
return $this->itemResponse(auth()->user());
}
return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
private function handleGoogleOauth() private function handleGoogleOauth()
{ {
$user = false; $user = false;

View File

@ -602,7 +602,18 @@ class CreditController extends BaseController
} }
break; break;
case 'email': case 'email':
// EmailCredit::dispatch($credit, $credit->company);
$credit->invitations->load('contact.client.country', 'credit.client.country', 'credit.company')->each(function ($invitation) use ($credit) {
EmailEntity::dispatch($invitation, $credit->company, 'credit');
});
if (! $bulk) {
return response()->json(['message'=>'email sent'], 200);
}
break;
case 'send_email':
$credit->invitations->load('contact.client.country', 'credit.client.country', 'credit.company')->each(function ($invitation) use ($credit) { $credit->invitations->load('contact.client.country', 'credit.client.country', 'credit.company')->each(function ($invitation) use ($credit) {
EmailEntity::dispatch($invitation, $credit->company, 'credit'); EmailEntity::dispatch($invitation, $credit->company, 'credit');

View File

@ -17,12 +17,15 @@ use App\Http\Middleware\UserVerified;
use App\Http\Requests\Email\SendEmailRequest; use App\Http\Requests\Email\SendEmailRequest;
use App\Jobs\Entity\EmailEntity; use App\Jobs\Entity\EmailEntity;
use App\Jobs\Mail\EntitySentMailer; use App\Jobs\Mail\EntitySentMailer;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote; use App\Models\Quote;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Transformers\CreditTransformer; use App\Transformers\CreditTransformer;
use App\Transformers\InvoiceTransformer; use App\Transformers\InvoiceTransformer;
use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\QuoteTransformer; use App\Transformers\QuoteTransformer;
use App\Transformers\RecurringInvoiceTransformer; use App\Transformers\RecurringInvoiceTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
@ -125,6 +128,10 @@ class EmailController extends BaseController
'body' => $body, 'body' => $body,
]; ];
if($entity == 'purchaseOrder' || $template == 'purchase_order'){
return $this->sendPurchaseOrder($entity_obj, $data);
}
$entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) { $entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) {
if (! $invitation->contact->trashed() && $invitation->contact->email) { if (! $invitation->contact->trashed() && $invitation->contact->email) {
$entity_obj->service()->markSent()->save(); $entity_obj->service()->markSent()->save();
@ -172,4 +179,17 @@ class EmailController extends BaseController
return $this->itemResponse($entity_obj->fresh()); return $this->itemResponse($entity_obj->fresh());
} }
private function sendPurchaseOrder($entity_obj, $data)
{
$this->entity_type = PurchaseOrder::class;
$this->entity_transformer = PurchaseOrderTransformer::class;
PurchaseOrderEmail::dispatch($entity_obj, $entity_obj->company, $data);
return $this->itemResponse($entity_obj);
}
} }

View File

@ -744,13 +744,7 @@ class InvoiceController extends BaseController
$this->itemResponse($invoice); $this->itemResponse($invoice);
} }
break; break;
// case 'reverse':
// $invoice = $invoice->service()->handleReversal()->deletePdf()->save();
// if (! $bulk) {
// $this->itemResponse($invoice);
// }
// break;
case 'email': case 'email':
//check query parameter for email_type and set the template else use calculateTemplate //check query parameter for email_type and set the template else use calculateTemplate
@ -767,6 +761,24 @@ class InvoiceController extends BaseController
} }
break; break;
case 'send_email':
//check query parameter for email_type and set the template else use calculateTemplate
if (request()->has('email_type') && property_exists($invoice->company->settings, request()->input('email_type'))) {
$this->reminder_template = $invoice->client->getSetting(request()->input('email_type'));
} else {
$this->reminder_template = $invoice->calculateTemplate('invoice');
}
BulkInvoiceJob::dispatch($invoice, $this->reminder_template);
if (! $bulk) {
return response()->json(['message' => 'email sent'], 200);
}
break;
default: default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400); return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
break; break;

View File

@ -272,7 +272,10 @@ class PreviewController extends BaseController
if (request()->query('html') == 'true') { if (request()->query('html') == 'true') {
return $maker->getCompiledHTML(); return $maker->getCompiledHTML();
} }
} catch (\Exception $e) {
}
catch(\Exception $e){
nlog($e->getMessage());
DB::connection(config('database.default'))->rollBack(); DB::connection(config('database.default'))->rollBack();
return; return;
@ -288,6 +291,9 @@ class PreviewController extends BaseController
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company()); $numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if ($numbered_pdf) { if ($numbered_pdf) {
$pdf = $numbered_pdf; $pdf = $numbered_pdf;
} }

View File

@ -0,0 +1,470 @@
<?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\Http\Controllers;
use App\DataMapper\Analytics\LivePreview;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PurchaseOrderFactory;
use App\Factory\QuoteFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Preview\PreviewInvoiceRequest;
use App\Http\Requests\Preview\PreviewPurchaseOrderRequest;
use App\Jobs\Util\PreviewPdf;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Repositories\CreditRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\PurchaseOrderRepository;
use App\Repositories\QuoteRepository;
use App\Repositories\RecurringInvoiceRepository;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\Design;
use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Utils\VendorHtmlEngine;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Response;
use Turbo124\Beacon\Facades\LightLogs;
class PreviewPurchaseOrderController extends BaseController
{
use MakesHash;
use MakesInvoiceHtml;
use PageNumbering;
public function __construct()
{
parent::__construct();
}
/**
* Returns a template filled with entity variables.
*
* @return \Illuminate\Http\Response
*
* @OA\Post(
* path="/api/v1/preview/purchase_order",
* operationId="getPreviewPurchaseOrder",
* tags={"preview"},
* summary="Returns a pdf preview for purchase order",
* description="Returns a pdf preview for purchase order.",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Response(
* response=200,
* description="The pdf response",
* @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\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 show()
{
if (request()->has('entity') &&
request()->has('entity_id') &&
! empty(request()->input('entity')) &&
! empty(request()->input('entity_id')) &&
request()->has('body')) {
$design_object = json_decode(json_encode(request()->input('design')));
if (! is_object($design_object)) {
return response()->json(['message' => ctrans('texts.invalid_design_object')], 400);
}
$entity_obj = PurchaseOrder::whereId($this->decodePrimaryKey(request()->input('entity_id')))->company()->first();
if (! $entity_obj) {
return $this->blankEntity();
}
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($entity_obj->company->locale());
$t->replace(Ninja::transformTranslations($entity_obj->company->settings));
$html = new VendorHtmlEngine($entity_obj->invitations()->first());
$design_namespace = 'App\Services\PdfMaker\Designs\\'.request()->design['name'];
$design_class = new $design_namespace();
$state = [
'template' => $design_class->elements([
'client' => null,
'vendor' => $entity_obj->vendor,
'entity' => $entity_obj,
'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables,
'variables' => $html->generateLabelsAndValues(),
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $entity_obj->company->markdown_enabled,
];
$design = new Design(request()->design['name']);
$maker = new PdfMaker($state);
$maker
->design($design)
->build();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
//if phantom js...... inject here..
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if($numbered_pdf)
$pdf = $numbered_pdf;
return $pdf;
}
//else
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
return $this->blankEntity();
}
public function live(PreviewPurchaseOrderRequest $request)
{
$company = auth()->user()->company();
MultiDB::setDb($company->db);
$repo = new PurchaseOrderRepository();
$entity_obj = PurchaseOrderFactory::create($company->id, auth()->user()->id);
$class = PurchaseOrder::class;
try {
DB::connection(config('database.default'))->beginTransaction();
if($request->has('entity_id')){
$entity_obj = $class::on(config('database.default'))
->with('vendor.company')
->where('id', $this->decodePrimaryKey($request->input('entity_id')))
->where('company_id', $company->id)
->withTrashed()
->first();
}
$entity_obj = $repo->save($request->all(), $entity_obj);
if(!$request->has('entity_id'))
$entity_obj->service()->fillDefaults()->save();
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($entity_obj->company->locale());
$t->replace(Ninja::transformTranslations($entity_obj->company->settings));
$html = new VendorHtmlEngine($entity_obj->invitations()->first());
$design = \App\Models\Design::find($entity_obj->design_id);
/* Catch all in case migration doesn't pass back a valid design */
if(!$design)
$design = \App\Models\Design::find(2);
if ($design->is_custom) {
$options = [
'custom_partials' => json_decode(json_encode($design->design), true)
];
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
} else {
$template = new PdfMakerDesign(strtolower($design->name));
}
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => null,
'vendor' => $entity_obj->vendor,
'entity' => $entity_obj,
'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables,
'variables' => $html->generateLabelsAndValues(),
'$product' => $design->design->product,
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $entity_obj->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($template)
->build();
DB::connection(config('database.default'))->rollBack();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
}
catch(\Exception $e){
DB::connection(config('database.default'))->rollBack();
return;
}
//if phantom js...... inject here..
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if($numbered_pdf)
$pdf = $numbered_pdf;
return $pdf;
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), $company);
if(Ninja::isHosted())
{
LightLogs::create(new LivePreview())
->increment()
->queue();
}
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
private function blankEntity()
{
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations(auth()->user()->company()->settings));
$invitation = PurchaseOrderInvitation::where('company_id', auth()->user()->company()->id)->orderBy('id', 'desc')->first();
/* If we don't have a valid invitation in the system - create a mock using transactions */
if(!$invitation)
return $this->mockEntity();
$design_object = json_decode(json_encode(request()->input('design')));
if (! is_object($design_object)) {
return response()->json(['message' => 'Invalid custom design object'], 400);
}
$html = new VendorHtmlEngine($invitation);
$design = new Design(Design::CUSTOM, ['custom_partials' => request()->design['design']]);
$state = [
'template' => $design->elements([
'client' => null,
'vendor' => $invitation->purchase_order->vendor,
'entity' => $invitation->purchase_order,
'pdf_variables' => (array) $invitation->company->settings->pdf_variables,
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $invitation->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($design)
->build();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if($numbered_pdf)
$pdf = $numbered_pdf;
return $pdf;
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
private function mockEntity()
{
DB::connection(auth()->user()->company()->db)->beginTransaction();
$vendor = Vendor::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
]);
$contact = VendorContact::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'vendor_id' => $vendor->id,
'is_primary' => 1,
'send_email' => true,
]);
$purchase_order = PurchaseOrder::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'vendor_id' => $vendor->id,
'terms' => 'Sample Terms',
'footer' => 'Sample Footer',
'public_notes' => 'Sample Public Notes',
]);
$invitation = PurchaseOrderInvitation::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'purchase_order_id' => $purchase_order->id,
'vendor_contact_id' => $contact->id,
]);
$purchase_order->setRelation('invitations', $invitation);
$purchase_order->setRelation('vendor', $vendor);
$purchase_order->setRelation('company', auth()->user()->company());
$purchase_order->load('vendor.company');
$design_object = json_decode(json_encode(request()->input('design')));
if (! is_object($design_object)) {
return response()->json(['message' => 'Invalid custom design object'], 400);
}
$html = new VendorHtmlEngine($purchase_order->invitations()->first());
$design = new Design(Design::CUSTOM, ['custom_partials' => request()->design['design']]);
$state = [
'template' => $design->elements([
'client' => null,
'vendor' => $purchase_order->vendor,
'entity' => $purchase_order,
'pdf_variables' => (array) $purchase_order->company->settings->pdf_variables,
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),
'process_markdown' => $purchase_order->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($design)
->build();
DB::connection(auth()->user()->company()->db)->rollBack();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company());
if($numbered_pdf)
$pdf = $numbered_pdf;
return $pdf;
}
$file_path = PreviewPdf::dispatchNow($maker->getCompiledHTML(true), auth()->user()->company());
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
}

View File

@ -11,6 +11,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Events\PurchaseOrder\PurchaseOrderWasCreated; use App\Events\PurchaseOrder\PurchaseOrderWasCreated;
use App\Events\PurchaseOrder\PurchaseOrderWasUpdated; use App\Events\PurchaseOrder\PurchaseOrderWasUpdated;
use App\Factory\PurchaseOrderFactory; use App\Factory\PurchaseOrderFactory;
@ -22,26 +23,30 @@ use App\Http\Requests\PurchaseOrder\EditPurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\ShowPurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\ShowPurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\StorePurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\StorePurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\UpdatePurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\UpdatePurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\UploadPurchaseOrderRequest;
use App\Jobs\Invoice\ZipInvoices; use App\Jobs\Invoice\ZipInvoices;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail; use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Jobs\PurchaseOrder\ZipPurchaseOrders; use App\Jobs\PurchaseOrder\ZipPurchaseOrders;
use App\Models\Account;
use App\Models\Client; use App\Models\Client;
use App\Models\Expense;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Repositories\PurchaseOrderRepository; use App\Repositories\PurchaseOrderRepository;
use App\Transformers\ExpenseTransformer;
use App\Transformers\PurchaseOrderTransformer; use App\Transformers\PurchaseOrderTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class PurchaseOrderController extends BaseController class PurchaseOrderController extends BaseController
{ {
use MakesHash; use MakesHash;
use SavesDocuments;
protected $entity_type = PurchaseOrder::class; protected $entity_type = PurchaseOrder::class;
protected $entity_transformer = PurchaseOrderTransformer::class; protected $entity_transformer = PurchaseOrderTransformer::class;
protected $purchase_order_repository; protected $purchase_order_repository;
public function __construct(PurchaseOrderRepository $purchase_order_repository) public function __construct(PurchaseOrderRepository $purchase_order_repository)
@ -50,7 +55,6 @@ class PurchaseOrderController extends BaseController
$this->purchase_order_repository = $purchase_order_repository; $this->purchase_order_repository = $purchase_order_repository;
} }
/** /**
* Show the list of Purchase Orders. * Show the list of Purchase Orders.
* *
@ -97,7 +101,6 @@ class PurchaseOrderController extends BaseController
return $this->listResponse($purchase_orders); return $this->listResponse($purchase_orders);
} }
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
@ -143,7 +146,6 @@ class PurchaseOrderController extends BaseController
return $this->itemResponse($purchase_order); return $this->itemResponse($purchase_order);
} }
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
@ -185,6 +187,7 @@ class PurchaseOrderController extends BaseController
*/ */
public function store(StorePurchaseOrderRequest $request) public function store(StorePurchaseOrderRequest $request)
{ {
$purchase_order = $this->purchase_order_repository->save($request->all(), PurchaseOrderFactory::create(auth()->user()->company()->id, auth()->user()->id)); $purchase_order = $this->purchase_order_repository->save($request->all(), PurchaseOrderFactory::create(auth()->user()->company()->id, auth()->user()->id));
$purchase_order = $purchase_order->service() $purchase_order = $purchase_order->service()
@ -196,7 +199,6 @@ class PurchaseOrderController extends BaseController
return $this->itemResponse($purchase_order); return $this->itemResponse($purchase_order);
} }
/** /**
* Display the specified resource. * Display the specified resource.
* *
@ -252,7 +254,6 @@ class PurchaseOrderController extends BaseController
{ {
return $this->itemResponse($purchase_order); return $this->itemResponse($purchase_order);
} }
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
@ -307,7 +308,6 @@ class PurchaseOrderController extends BaseController
{ {
return $this->itemResponse($purchase_order); return $this->itemResponse($purchase_order);
} }
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
@ -367,11 +367,14 @@ class PurchaseOrderController extends BaseController
$purchase_order = $this->purchase_order_repository->save($request->all(), $purchase_order); $purchase_order = $this->purchase_order_repository->save($request->all(), $purchase_order);
$purchase_order = $purchase_order->service()
->triggeredActions($request)
->save();
event(new PurchaseOrderWasUpdated($purchase_order, $purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new PurchaseOrderWasUpdated($purchase_order, $purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
return $this->itemResponse($purchase_order); return $this->itemResponse($purchase_order);
} }
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *
@ -481,6 +484,7 @@ class PurchaseOrderController extends BaseController
*/ */
public function bulk() public function bulk()
{ {
$action = request()->input('action'); $action = request()->input('action');
$ids = request()->input('ids'); $ids = request()->input('ids');
@ -498,8 +502,7 @@ class PurchaseOrderController extends BaseController
if ($action == 'bulk_download' && $purchase_orders->count() >= 1) { if ($action == 'bulk_download' && $purchase_orders->count() >= 1) {
$purchase_orders->each(function ($purchase_order) { $purchase_orders->each(function ($purchase_order) {
if (auth()->user()->cannot('view', $purchase_order)) { if (auth()->user()->cannot('view', $purchase_order)) {
nlog('access denied'); nlog("access denied");
return response()->json(['message' => ctrans('text.access_denied')]); return response()->json(['message' => ctrans('text.access_denied')]);
} }
}); });
@ -643,10 +646,113 @@ class PurchaseOrderController extends BaseController
if (! $bulk) { if (! $bulk) {
return response()->json(['message' => 'email sent'], 200); return response()->json(['message' => 'email sent'], 200);
} }
break;
case 'send_email':
//check query parameter for email_type and set the template else use calculateTemplate
PurchaseOrderEmail::dispatch($purchase_order, $purchase_order->company);
if (! $bulk) {
return response()->json(['message' => 'email sent'], 200);
}
break;
case 'add_to_inventory':
$purchase_order->service()->add_to_inventory();
return $this->itemResponse($purchase_order);
case 'expense':
if($purchase_order->expense()->exists())
return response()->json(['message' => ctrans('texts.purchase_order_already_expensed')], 400);
$expense = $purchase_order->service()->expense();
return $this->itemResponse($purchase_order);
case 'cancel':
if($purchase_order->status_id <= PurchaseOrder::STATUS_SENT)
{
$purchase_order->status_id = PurchaseOrder::STATUS_CANCELLED;
$purchase_order->save();
}
if (! $bulk) {
return $this->listResponse($purchase_order);
}
break;
default: default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400); return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
break; break;
} }
} }
/**
* Update the specified resource in storage.
*
* @param UploadPurchaseOrderRequest $request
* @param PurchaseOrder $purchase_order
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/purchase_orders/{id}/upload",
* operationId="uploadPurchaseOrder",
* tags={"purchase_orders"},
* summary="Uploads a document to a purchase_orders",
* description="Handles the uploading of a document to a purchase_order",
* @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\Parameter(
* name="id",
* in="path",
* description="The Purchase Order Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Purchase Order 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/Vendor"),
* ),
* @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 upload(UploadPurchaseOrderRequest $request, PurchaseOrder $purchase_order)
{
if(!$this->checkFeature(Account::FEATURE_DOCUMENTS))
return $this->featureFailure();
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $purchase_order);
return $this->itemResponse($purchase_order->fresh());
}
} }

View File

@ -723,6 +723,13 @@ class QuoteController extends BaseController
return response()->json(['message'=> ctrans('texts.sent_message')], 200); return response()->json(['message'=> ctrans('texts.sent_message')], 200);
break; break;
case 'send_email':
$quote->service()->sendEmail();
return response()->json(['message'=> ctrans('texts.sent_message')], 200);
break;
case 'mark_sent': case 'mark_sent':
$quote->service()->markSent()->save(); $quote->service()->markSent()->save();

View File

@ -204,10 +204,6 @@ class RecurringInvoiceController extends BaseController
{ {
$recurring_invoice = $this->recurring_invoice_repo->save($request->all(), RecurringInvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id)); $recurring_invoice = $this->recurring_invoice_repo->save($request->all(), RecurringInvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id));
// $offset = $recurring_invoice->client->timezone_offset();
// $recurring_invoice->next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay()->addSeconds($offset);
// $recurring_invoice->saveQuietly();
$recurring_invoice->service() $recurring_invoice->service()
->triggeredActions($request) ->triggeredActions($request)
->save(); ->save();
@ -700,6 +696,15 @@ class RecurringInvoiceController extends BaseController
$this->itemResponse($recurring_invoice); $this->itemResponse($recurring_invoice);
} }
break;
case 'send_now':
$recurring_invoice = $recurring_invoice->service()->sendNow();
if (! $bulk) {
$this->itemResponse($recurring_invoice);
}
break; break;
default: default:
// code... // code...

View File

@ -21,6 +21,7 @@ use App\Models\Company;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\PaymentDrivers\Stripe\Connect\Account; use App\PaymentDrivers\Stripe\Connect\Account;
use App\PaymentDrivers\Stripe\Jobs\StripeWebhook;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Stripe\Exception\ApiErrorException; use Stripe\Exception\ApiErrorException;
@ -114,6 +115,8 @@ class StripeConnectController extends BaseController
$company_gateway->setConfig($payload); $company_gateway->setConfig($payload);
$company_gateway->save(); $company_gateway->save();
StripeWebhook::dispatch($company->company_key, $company_gateway->id);
//response here //response here
return view('auth.connect.completed'); return view('auth.connect.completed');
} }

View File

@ -64,9 +64,11 @@ class UserController extends BaseController
*/ */
public function __construct(UserRepository $user_repo) public function __construct(UserRepository $user_repo)
{ {
parent::__construct(); parent::__construct();
$this->user_repo = $user_repo; $this->user_repo = $user_repo;
} }
/** /**
@ -156,7 +158,7 @@ class UserController extends BaseController
*/ */
public function create(CreateUserRequest $request) public function create(CreateUserRequest $request)
{ {
$user = UserFactory::create(auth()->user()->account->id); $user = UserFactory::create(auth()->user()->account_id);
return $this->itemResponse($user); return $this->itemResponse($user);
} }
@ -208,7 +210,7 @@ class UserController extends BaseController
$user_agent = request()->input('token_name') ?: request()->server('HTTP_USER_AGENT'); $user_agent = request()->input('token_name') ?: request()->server('HTTP_USER_AGENT');
$ct = (new CreateCompanyToken($company, $user, $user_agent))->handle(); $ct = CreateCompanyToken::dispatchNow($company, $user, $user_agent);
event(new UserWasCreated($user, auth()->user(), $company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new UserWasCreated($user, auth()->user(), $company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
@ -394,7 +396,7 @@ class UserController extends BaseController
UserEmailChanged::dispatch($new_user, json_decode($old_user), auth()->user()->company()); UserEmailChanged::dispatch($new_user, json_decode($old_user), auth()->user()->company());
} }
$user->company_users()->update(['permissions_updated_at' => now()]); // $user->company_users()->update(["permissions_updated_at" => now()]);
event(new UserWasUpdated($user, auth()->user(), auth()->user()->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new UserWasUpdated($user, auth()->user(), auth()->user()->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
@ -463,9 +465,8 @@ class UserController extends BaseController
*/ */
public function destroy(DestroyUserRequest $request, User $user) public function destroy(DestroyUserRequest $request, User $user)
{ {
if ($user->isOwner()) { if($user->isOwner())
return response()->json(['message', 'Cannot detach owner.'],400); return response()->json(['message', 'Cannot detach owner.'],400);
}
/* If the user passes the company user we archive the company user */ /* If the user passes the company user we archive the company user */
$user = $this->user_repo->delete($request->all(), $user); $user = $this->user_repo->delete($request->all(), $user);
@ -605,6 +606,7 @@ class UserController extends BaseController
*/ */
public function detach(DetachCompanyUserRequest $request, User $user) public function detach(DetachCompanyUserRequest $request, User $user)
{ {
if ($request->entityIsDeleted($user)) { if ($request->entityIsDeleted($user)) {
return $request->disallowUpdate(); return $request->disallowUpdate();
} }
@ -614,9 +616,8 @@ class UserController extends BaseController
->withTrashed() ->withTrashed()
->first(); ->first();
if ($company_user->is_owner) { if($company_user->is_owner)
return response()->json(['message', 'Cannot detach owner.'], 401); return response()->json(['message', 'Cannot detach owner.'], 401);
}
$token = $company_user->token->where('company_id', $company_user->company_id)->where('user_id', $company_user->user_id)->first(); $token = $company_user->token->where('company_id', $company_user->company_id)->where('user_id', $company_user->user_id)->first();
@ -680,11 +681,14 @@ class UserController extends BaseController
*/ */
public function invite(ReconfirmUserRequest $request, User $user) public function invite(ReconfirmUserRequest $request, User $user)
{ {
$user->service()->invite($user->company()); $user->service()->invite($user->company());
return response()->json(['message' => ctrans('texts.confirmation_resent')], 200); return response()->json(['message' => ctrans('texts.confirmation_resent')], 200);
} }
/** /**
* Invite an existing user to a company. * Invite an existing user to a company.
* *
@ -734,8 +738,10 @@ class UserController extends BaseController
*/ */
public function reconfirm(ReconfirmUserRequest $request, User $user) public function reconfirm(ReconfirmUserRequest $request, User $user)
{ {
$user->service()->invite($user->company()); $user->service()->invite($user->company());
return response()->json(['message' => ctrans('texts.confirmation_resent')], 200); return response()->json(['message' => ctrans('texts.confirmation_resent')], 200);
} }
} }

View File

@ -17,11 +17,13 @@ use App\Events\Misc\InvitationWasViewed;
use App\Events\Quote\QuoteWasViewed; use App\Events\Quote\QuoteWasViewed;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf; use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\CreditInvitation; use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation; use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation; use App\Models\QuoteInvitation;
use App\Services\ClientPortal\InstantPayment; use App\Services\ClientPortal\InstantPayment;
@ -43,6 +45,7 @@ class InvitationController extends Controller
public function purchaseOrder(string $invitation_key) public function purchaseOrder(string $invitation_key)
{ {
Auth::logout(); Auth::logout();
$invitation = PurchaseOrderInvitation::withTrashed() $invitation = PurchaseOrderInvitation::withTrashed()
@ -53,27 +56,24 @@ class InvitationController extends Controller
->with('contact.vendor') ->with('contact.vendor')
->first(); ->first();
if (! $invitation) { if(!$invitation)
return abort(404,'The resource is no longer available.'); return abort(404,'The resource is no longer available.');
}
if ($invitation->contact->trashed()) { if($invitation->contact->trashed())
$invitation->contact->restore(); $invitation->contact->restore();
}
$vendor_contact = $invitation->contact; $vendor_contact = $invitation->contact;
$entity = 'purchase_order'; $entity = 'purchase_order';
if (empty($vendor_contact->email)) { if(empty($vendor_contact->email))
$vendor_contact->email = Str::random(15).'@example.com'; $vendor_contact->email = Str::random(15) . "@example.com"; $vendor_contact->save();
}
$vendor_contact->save();
if (request()->has('vendor_hash') && request()->input('vendor_hash') == $invitation->contact->vendor->vendor_hash) { if (request()->has('vendor_hash') && request()->input('vendor_hash') == $invitation->contact->vendor->vendor_hash) {
request()->session()->invalidate(); request()->session()->invalidate();
auth()->guard('vendor')->loginUsingId($vendor_contact->id, true); auth()->guard('vendor')->loginUsingId($vendor_contact->id, true);
} else { } else {
nlog('else - default - login contact'); nlog("else - default - login contact");
request()->session()->invalidate(); request()->session()->invalidate();
auth()->guard('vendor')->loginUsingId($vendor_contact->id, true); auth()->guard('vendor')->loginUsingId($vendor_contact->id, true);
} }
@ -81,55 +81,49 @@ class InvitationController extends Controller
session()->put('is_silent', request()->has('silent')); session()->put('is_silent', request()->has('silent'));
if (auth()->guard('vendor')->user() && ! session()->get('is_silent') && ! $invitation->viewed_date) { if (auth()->guard('vendor')->user() && ! session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed(); $invitation->markViewed();
event(new InvitationWasViewed($invitation->purchase_order, $invitation, $invitation->company, Ninja::eventVars())); event(new InvitationWasViewed($invitation->purchase_order, $invitation, $invitation->company, Ninja::eventVars()));
} else {
}
else{
return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id), 'silent' => session()->get('is_silent')]); return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id), 'silent' => session()->get('is_silent')]);
} }
return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id)]); return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id)]);
} }
// public function routerForDownload(string $entity, string $invitation_key) public function download(string $invitation_key)
// { {
$invitation = PurchaseOrderInvitation::withTrashed()
->where('key', $invitation_key)
->with('contact.vendor')
->firstOrFail();
// set_time_limit(45); if(!$invitation)
return response()->json(["message" => "no record found"], 400);
// if(Ninja::isHosted()) $file_name = $invitation->purchase_order->numberFormatter().'.pdf';
// return $this->returnRawPdf($entity, $invitation_key);
// return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf');
// }
// private function returnRawPdf(string $entity, string $invitation_key)
// {
// if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
// return response()->json(['message' => 'Invalid resource request']);
// $key = $entity.'_id';
// $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
// $invitation = $entity_obj::where('key', $invitation_key)
// ->with('contact.client')
// ->firstOrFail();
// if(!$invitation)
// return response()->json(["message" => "no record found"], 400);
// $file_name = $invitation->purchase_order->numberFormatter().'.pdf';
// $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db); // $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db);
// $headers = ['Content-Type' => 'application/pdf']; $file = (new CreatePurchaseOrderPdf($invitation))->rawPdf();
$headers = ['Content-Type' => 'application/pdf'];
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
return response()->streamDownload(function () use($file) {
echo $file;
}, $file_name, $headers);
}
// if(request()->input('inline') == 'true')
// $headers = array_merge($headers, ['Content-Disposition' => 'inline']);
// return response()->streamDownload(function () use($file) {
// echo $file;
// }, $file_name, $headers);
// }
} }

View File

@ -0,0 +1,41 @@
<?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\Http\Controllers\VendorPortal;
use App\Http\Controllers\Controller;
use App\Http\Requests\VendorPortal\Uploads\StoreUploadRequest;
use App\Models\PurchaseOrder;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Response;
class UploadController extends Controller
{
use SavesDocuments;
use MakesHash;
/**
* Main logic behind uploading the files.
*
* @param StoreUploadRequest $request
* @return Response|ResponseFactory
*/
public function upload(StoreUploadRequest $request, PurchaseOrder $purchase_order)
{
$this->saveDocuments($request->getFile(), $purchase_order, true);
return response([], 200);
}
}

View File

@ -44,6 +44,7 @@ use App\Http\Middleware\UrlSetDb;
use App\Http\Middleware\UserVerified; use App\Http\Middleware\UserVerified;
use App\Http\Middleware\VendorLocale; use App\Http\Middleware\VendorLocale;
use App\Http\Middleware\VerifyCsrfToken; use App\Http\Middleware\VerifyCsrfToken;
use App\Http\Middleware\VerifyHash;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified; use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
@ -161,6 +162,7 @@ class Kernel extends HttpKernel
'locale' => Locale::class, 'locale' => Locale::class,
'vendor_locale' => VendorLocale::class, 'vendor_locale' => VendorLocale::class,
'contact_register' => ContactRegister::class, 'contact_register' => ContactRegister::class,
'verify_hash' => VerifyHash::class,
'shop_token_auth' => ShopTokenAuth::class, 'shop_token_auth' => ShopTokenAuth::class,
'phantom_secret' => PhantomSecret::class, 'phantom_secret' => PhantomSecret::class,
'contact_key_login' => ContactKeyLogin::class, 'contact_key_login' => ContactKeyLogin::class,

View File

@ -11,7 +11,6 @@
namespace App\Http\Livewire; namespace App\Http\Livewire;
use App\DataMapper\ClientSettings;
use App\Factory\ClientFactory; use App\Factory\ClientFactory;
use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\Mail\NinjaMailerObject;
@ -28,6 +27,7 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\DataMapper\ClientSettings;
use Livewire\Component; use Livewire\Component;
class BillingPortalPurchase extends Component class BillingPortalPurchase extends Component
@ -46,6 +46,7 @@ class BillingPortalPurchase extends Component
*/ */
public $heading_text; public $heading_text;
/** /**
* E-mail address model for user input. * E-mail address model for user input.
* *
@ -86,14 +87,14 @@ class BillingPortalPurchase extends Component
/** /**
* Id for CompanyGateway record. * Id for CompanyGateway record.
* *
* @var string|int * @var string|integer
*/ */
public $company_gateway_id; public $company_gateway_id;
/** /**
* Id for GatewayType. * Id for GatewayType.
* *
* @var string|int * @var string|integer
*/ */
public $payment_method_id; public $payment_method_id;
@ -143,7 +144,7 @@ class BillingPortalPurchase extends Component
* *
* @var int * @var int
*/ */
public $quantity = 1; public $quantity;
/** /**
* First-hit request data (queries, locales...). * First-hit request data (queries, locales...).
@ -182,12 +183,15 @@ class BillingPortalPurchase extends Component
{ {
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
$this->quantity = 1;
$this->price = $this->subscription->price; $this->price = $this->subscription->price;
if (request()->query('coupon')) { if (request()->query('coupon')) {
$this->coupon = request()->query('coupon'); $this->coupon = request()->query('coupon');
$this->handleCoupon(); $this->handleCoupon();
} elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) { }
elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0){
$this->price = $this->subscription->promo_price; $this->price = $this->subscription->promo_price;
} }
} }
@ -295,11 +299,10 @@ class BillingPortalPurchase extends Component
return $this; return $this;
} }
if ((int) $this->price == 0) { if ((int)$this->price == 0)
$this->steps['payment_required'] = false; $this->steps['payment_required'] = false;
} else { else
$this->steps['fetched_payment_methods'] = true; $this->steps['fetched_payment_methods'] = true;
}
$this->methods = $contact->client->service()->getPaymentMethods($this->price); $this->methods = $contact->client->service()->getPaymentMethods($this->price);
@ -357,7 +360,7 @@ class BillingPortalPurchase extends Component
$this->invoice = $this->subscription $this->invoice = $this->subscription
->service() ->service()
->createInvoice($data) ->createInvoice($data, $this->quantity)
->service() ->service()
->markSent() ->markSent()
->fillDefaults() ->fillDefaults()
@ -393,6 +396,7 @@ class BillingPortalPurchase extends Component
public function handlePaymentNotRequired() public function handlePaymentNotRequired()
{ {
$is_eligible = $this->subscription->service()->isEligible($this->contact); $is_eligible = $this->subscription->service()->isEligible($this->contact);
if ($is_eligible['status_code'] != 200) { if ($is_eligible['status_code'] != 200) {
@ -403,6 +407,7 @@ class BillingPortalPurchase extends Component
return; return;
} }
return $this->subscription->service()->handleNoPaymentRequired([ return $this->subscription->service()->handleNoPaymentRequired([
'email' => $this->email ?? $this->contact->email, 'email' => $this->email ?? $this->contact->email,
'quantity' => $this->quantity, 'quantity' => $this->quantity,
@ -430,14 +435,14 @@ class BillingPortalPurchase extends Component
if ($option == 'increment') { if ($option == 'increment') {
$this->quantity++; $this->quantity++;
$this->price = $this->subscription->promo_price * $this->quantity;
return $this->price = (int) $this->price + $this->subscription->product->price; return $this->quantity;
} }
$this->quantity--; $this->quantity--;
$this->price = (int) $this->price - $this->subscription->product->price; $this->price = $this->subscription->promo_price * $this->quantity;
return 0; return $this->quantity;
} }
public function handleCoupon() public function handleCoupon()
@ -463,7 +468,7 @@ class BillingPortalPurchase extends Component
$mailer->settings = $this->subscription->company->settings; $mailer->settings = $this->subscription->company->settings;
$mailer->to_user = $contact; $mailer->to_user = $contact;
NinjaMailerJob::dispatchSync($mailer); NinjaMailerJob::dispatchNow($mailer);
$this->steps['passwordless_login_sent'] = true; $this->steps['passwordless_login_sent'] = true;
$this->passwordless_login_btn = false; $this->passwordless_login_btn = false;

View File

@ -72,6 +72,12 @@ class RequiredClientInfo extends Component
'state', 'state',
'postal_code', 'postal_code',
'country_id', 'country_id',
'shipping_address1',
'shipping_address2',
'shipping_city',
'shipping_state',
'shipping_postal_code',
'shipping_country_id',
]; ];
protected $rules = [ protected $rules = [

View File

@ -31,6 +31,7 @@ class PasswordProtection
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
$error = [ $error = [
'message' => 'Invalid Password', 'message' => 'Invalid Password',
'errors' => new stdClass, 'errors' => new stdClass,
@ -38,61 +39,95 @@ class PasswordProtection
$timeout = auth()->user()->company()->default_password_timeout; $timeout = auth()->user()->company()->default_password_timeout;
if ($timeout == 0) { if($timeout == 0)
$timeout = 30*60*1000*1000; $timeout = 30*60*1000*1000;
} else { else
$timeout = $timeout/1000; $timeout = $timeout/1000;
}
//test if password if base64 encoded //test if password if base64 encoded
$x_api_password = $request->header('X-API-PASSWORD'); $x_api_password = $request->header('X-API-PASSWORD');
if ($request->header('X-API-PASSWORD-BASE64')) { if($request->header('X-API-PASSWORD-BASE64'))
{
$x_api_password = base64_decode($request->header('X-API-PASSWORD-BASE64')); $x_api_password = base64_decode($request->header('X-API-PASSWORD-BASE64'));
} }
// If no password supplied - then we just check if their authentication is in cache // // 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) { 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); Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
return $next($request); return $next($request);
}elseif( $request->header('X-API-OAUTH-PASSWORD') && strlen($request->header('X-API-OAUTH-PASSWORD')) >=1){ }elseif( $request->header('X-API-OAUTH-PASSWORD') && strlen($request->header('X-API-OAUTH-PASSWORD')) >=1){
//user is attempting to reauth with OAuth - check the token value //user is attempting to reauth with OAuth - check the token value
//todo expand this to include all OAuth providers //todo expand this to include all OAuth providers
if(auth()->user()->oauth_provider_id == 'google')
{
$user = false; $user = false;
$google = new Google(); $google = new Google();
$user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD')); $user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD'));
if (is_array($user)) { if (is_array($user)) {
$query = [ $query = [
'oauth_user_id' => $google->harvestSubField($user), 'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id'=> 'google', 'oauth_provider_id'=> 'google'
]; ];
//If OAuth and user also has a password set - check both //If OAuth and user also has a password set - check both
if ($existing_user = MultiDB::hasUser($query) && auth()->user()->company()->oauth_password_required && auth()->user()->has_password && Hash::check(auth()->user()->password, $x_api_password)) { if ($existing_user = MultiDB::hasUser($query) && auth()->user()->company()->oauth_password_required && auth()->user()->has_password && Hash::check(auth()->user()->password, $x_api_password)) {
nlog('existing user with password');
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); nlog("existing user with password");
return $next($request);
} elseif ($existing_user = MultiDB::hasUser($query) && ! auth()->user()->company()->oauth_password_required) {
nlog('existing user without password');
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
return $next($request); return $next($request);
} }
elseif($existing_user = MultiDB::hasUser($query) && !auth()->user()->company()->oauth_password_required){
nlog("existing user without password");
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
return $next($request);
} }
}
}
elseif(auth()->user()->oauth_provider_id == 'microsoft')
{
try{
$payload = json_decode(base64_decode(str_replace('_', '/', str_replace('-','+',explode('.', request()->header('X-API-OAUTH-PASSWORD'))[1]))));
}
catch(\Exception $e){
nlog("could not decode microsoft response");
return response()->json(['message' => 'Could not decode the response from Microsoft'], 412);
}
if($payload->preferred_username == 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); return response()->json($error, 412);
}elseif ($x_api_password && Hash::check($x_api_password, auth()->user()->password)) { }elseif ($x_api_password && Hash::check($x_api_password, auth()->user()->password)) {
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
return $next($request); return $next($request);
} else { } else {
return response()->json($error, 412); return response()->json($error, 412);
} }
} }
} }

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Middleware;
use App\Models\Account;
use App\Models\Company;
use App\Models\PaymentHash;
use App\Utils\Ninja;
use Closure;
use Illuminate\Http\Request;
class VerifyHash
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if($request->has('payment_hash')){
$ph = PaymentHash::with('fee_invoice')->where('hash', $request->payment_hash)->first();
if($ph)
auth()->guard('contact')->loginUsingId($ph->fee_invoice->invitations->first()->contact->id, true);
return $next($request);
}
abort(404, 'Unable to verify payment hash');
}
}

View File

@ -18,7 +18,7 @@ class ShowRecurringInvoiceRequest extends Request
{ {
public function authorize() : bool public function authorize() : bool
{ {
return auth()->guard('contact')->user()->client->id === $this->recurring_invoice->client_id return auth()->guard('contact')->user()->client->id == $this->recurring_invoice->client_id
&& auth()->guard('contact')->user()->company->enabled_modules & PortalComposer::MODULE_RECURRING_INVOICES; && auth()->guard('contact')->user()->company->enabled_modules & PortalComposer::MODULE_RECURRING_INVOICES;
} }

View File

@ -1,4 +1,14 @@
<?php <?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\Http\Requests\ClientPortal\Uploads; namespace App\Http\Requests\ClientPortal\Uploads;

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\Email;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Support\Str;
class SendEmailRequest extends Request class SendEmailRequest extends Request
{ {
@ -42,7 +43,7 @@ class SendEmailRequest extends Request
]; ];
} }
public function prepareForValidation() protected function prepareForValidation()
{ {
$input = $this->all(); $input = $this->all();
@ -56,13 +57,11 @@ class SendEmailRequest extends Request
unset($input['template']); unset($input['template']);
} }
if (array_key_exists('entity_id', $input)) { if(array_key_exists('entity_id', $input))
$input['entity_id'] = $this->decodePrimaryKey($input['entity_id']); $input['entity_id'] = $this->decodePrimaryKey($input['entity_id']);
}
if (array_key_exists('entity', $input)) { if(array_key_exists('entity', $input))
$input['entity'] = "App\Models\\".ucfirst($input['entity']); $input['entity'] = "App\Models\\".ucfirst(Str::camel($input['entity']));
}
$this->replace($input); $this->replace($input);
} }

View File

@ -14,6 +14,7 @@ namespace App\Http\Requests\Expense;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Expense\UniqueExpenseNumberRule; use App\Http\ValidationRules\Expense\UniqueExpenseNumberRule;
use App\Models\Expense; use App\Models\Expense;
use App\Models\PurchaseOrder;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;

View File

@ -0,0 +1,62 @@
<?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\Http\Requests\Preview;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Project\ValidProjectForClient;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class PreviewPurchaseOrderRequest extends Request
{
use MakesHash;
use CleanLineItems;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('create', PurchaseOrder::class);
}
public function rules()
{
$rules = [];
$rules['number'] = ['nullable'];
return $rules;
}
protected function prepareForValidation()
{
$input = $this->all();
$input = $this->decodePrimaryKeys($input);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
$input['amount'] = 0;
$input['balance'] = 0;
$input['number'] = ctrans('texts.live_preview') . " #". rand(0,1000);
$this->replace($input);
}
}

View File

@ -58,6 +58,6 @@ class StoreProjectRequest extends Request
public function getClient($client_id) public function getClient($client_id)
{ {
return Client::find($client_id); return Client::withTrashed()->find($client_id);
} }
} }

View File

@ -11,14 +11,17 @@
namespace App\Http\Requests\PurchaseOrder; namespace App\Http\Requests\PurchaseOrder;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class StorePurchaseOrderRequest extends Request class StorePurchaseOrderRequest extends Request
{ {
use MakesHash; use MakesHash;
use CleanLineItems;
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
@ -29,7 +32,6 @@ class StorePurchaseOrderRequest extends Request
{ {
return auth()->user()->can('create', PurchaseOrder::class); return auth()->user()->can('create', PurchaseOrder::class);
} }
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
@ -44,18 +46,24 @@ class StorePurchaseOrderRequest extends Request
$rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', auth()->user()->company()->id)]; $rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', auth()->user()->company()->id)];
$rules['discount'] = 'sometimes|numeric'; $rules['discount'] = 'sometimes|numeric';
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
return $rules; return $rules;
} }
public function prepareForValidation() protected function prepareForValidation()
{ {
$input = $this->all(); $input = $this->all();
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
if (isset($input['line_items']) && is_array($input['line_items']))
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
$input['amount'] = 0;
$input['balance'] = 0;
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\PurchaseOrder;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -20,6 +21,7 @@ class UpdatePurchaseOrderRequest extends Request
{ {
use ChecksEntityStatus; use ChecksEntityStatus;
use MakesHash; use MakesHash;
use CleanLineItems;
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
@ -59,6 +61,10 @@ class UpdatePurchaseOrderRequest extends Request
$input['id'] = $this->purchase_order->id; $input['id'] = $this->purchase_order->id;
if (isset($input['line_items']) && is_array($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -0,0 +1,39 @@
<?php
/**
* Quote Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2022. Quote Ninja LLC (https://paymentninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\PurchaseOrder;
use App\Http\Requests\Request;
class UploadPurchaseOrderRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->purchase_order);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:csv,png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* Quote Ninja (https://paymentninja.com). * Invoice Ninja (https://paymentninja.com).
* *
* @link https://github.com/paymentninja/paymentninja source repository * @link https://github.com/paymentninja/paymentninja source repository
* *
* @copyright Copyright (c) 2022. Quote Ninja LLC (https://paymentninja.com) * @copyright Copyright (c) 2022. Invoice Ninja LLC (https://paymentninja.com)
* *
* @license https://www.elastic.co/licensing/elastic-license * @license https://www.elastic.co/licensing/elastic-license
*/ */
@ -23,7 +23,7 @@ class SortTaskRequest extends Request
public function authorize() : bool public function authorize() : bool
{ {
return true; return true;
// return auth()->user()->can('edit', $this->task);
} }
public function rules() public function rules()

View File

@ -0,0 +1,55 @@
<?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\Http\Requests\VendorPortal\Uploads;
use Illuminate\Foundation\Http\FormRequest;
class StoreUploadRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return (bool) auth()->guard('vendor')->user()->vendor->company->getSetting('vendor_portal_enable_uploads');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'file' => ['file', 'mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'],
];
}
/**
* Since saveDocuments() expects an array of uploaded files,
* we need to convert it to an array before uploading.
*
* @return mixed
*/
public function getFile()
{
if (gettype($this->file) !== 'array') {
return [$this->file];
}
return $this->file;
}
}

View File

@ -44,7 +44,7 @@ class ValidCreditsPresentRule implements Rule
{ {
//todo need to ensure the clients credits are here not random ones! //todo need to ensure the clients credits are here not random ones!
if (request()->input('credits') && is_array(request()->input('credits'))) { if (request()->input('credits') && is_array(request()->input('credits')) && count(request()->input('credits')) > 0) {
$credit_collection = Credit::whereIn('id', $this->transformKeys(array_column(request()->input('credits'), 'credit_id'))) $credit_collection = Credit::whereIn('id', $this->transformKeys(array_column(request()->input('credits'), 'credit_id')))
->count(); ->count();

File diff suppressed because it is too large Load Diff

View File

@ -37,11 +37,11 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang; use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Turbo124\Beacon\Facades\LightLogs; use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Support\Facades\Cache;
/*Multi Mailer implemented*/ /*Multi Mailer implemented*/
@ -65,8 +65,10 @@ class NinjaMailerJob implements ShouldQueue
public function __construct(NinjaMailerObject $nmo, bool $override = false) public function __construct(NinjaMailerObject $nmo, bool $override = false)
{ {
$this->nmo = $nmo; $this->nmo = $nmo;
$this->override = $override; $this->override = $override;
} }
public function handle() public function handle()
@ -78,29 +80,30 @@ class NinjaMailerJob implements ShouldQueue
/* Serializing models from other jobs wipes the primary key */ /* Serializing models from other jobs wipes the primary key */
$this->company = Company::where('company_key', $this->nmo->company->company_key)->first(); $this->company = Company::where('company_key', $this->nmo->company->company_key)->first();
if ($this->preFlightChecksFail()) { if($this->preFlightChecksFail())
return; return;
}
/* Set the email driver */ /* Set the email driver */
$this->setMailDriver(); $this->setMailDriver();
if (strlen($this->nmo->settings->reply_to_email) > 1) { if (strlen($this->nmo->settings->reply_to_email) > 1) {
if (property_exists($this->nmo->settings, 'reply_to_name')) {
if(property_exists($this->nmo->settings, 'reply_to_name'))
$reply_to_name = strlen($this->nmo->settings->reply_to_name) > 3 ? $this->nmo->settings->reply_to_name : $this->nmo->settings->reply_to_email; $reply_to_name = strlen($this->nmo->settings->reply_to_name) > 3 ? $this->nmo->settings->reply_to_name : $this->nmo->settings->reply_to_email;
} else { else
$reply_to_name = $this->nmo->settings->reply_to_email; $reply_to_name = $this->nmo->settings->reply_to_email;
}
$this->nmo->mailable->replyTo($this->nmo->settings->reply_to_email, $reply_to_name); $this->nmo->mailable->replyTo($this->nmo->settings->reply_to_email, $reply_to_name);
} else {
}
else {
$this->nmo->mailable->replyTo($this->company->owner()->email, $this->company->owner()->present()->name()); $this->nmo->mailable->replyTo($this->company->owner()->email, $this->company->owner()->present()->name());
} }
//send email //send email
try { try {
nlog("trying to send to {$this->nmo->to_user->email} ". now()->toDateTimeString()); nlog("trying to send to {$this->nmo->to_user->email} ". now()->toDateTimeString());
nlog('Using mailer => '.$this->mailer); nlog("Using mailer => ". $this->mailer);
Mail::mailer($this->mailer) Mail::mailer($this->mailer)
->to($this->nmo->to_user->email) ->to($this->nmo->to_user->email)
@ -111,7 +114,9 @@ class NinjaMailerJob implements ShouldQueue
/* Count the amount of emails sent across all the users accounts */ /* Count the amount of emails sent across all the users accounts */
Cache::increment($this->company->account->key); Cache::increment($this->company->account->key);
} catch (\Exception $e) { } catch (\Exception $e) {
nlog("error failed with {$e->getMessage()}"); nlog("error failed with {$e->getMessage()}");
$message = $e->getMessage(); $message = $e->getMessage();
@ -130,19 +135,18 @@ class NinjaMailerJob implements ShouldQueue
$message = $message_body->Message; $message = $message_body->Message;
nlog($message); nlog($message);
} }
} }
/* If the is an entity attached to the message send a failure mailer */ /* If the is an entity attached to the message send a failure mailer */
if ($this->nmo->entity) { if($this->nmo->entity)
$this->entityEmailFailed($message); $this->entityEmailFailed($message);
}
/* Don't send postmark failures to Sentry */ /* Don't send postmark failures to Sentry */
if (Ninja::isHosted() && (! $e instanceof ClientException)) { if(Ninja::isHosted() && (!$e instanceof ClientException))
app('sentry')->captureException($e); app('sentry')->captureException($e);
} }
} }
}
/* Switch statement to handle failure notifications */ /* Switch statement to handle failure notifications */
private function entityEmailFailed($message) private function entityEmailFailed($message)
@ -157,14 +161,13 @@ class NinjaMailerJob implements ShouldQueue
event(new PaymentWasEmailedAndFailed($this->nmo->entity, $this->nmo->company, $message, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new PaymentWasEmailedAndFailed($this->nmo->entity, $this->nmo->company, $message, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
break; break;
default: default:
// code... # code...
break; break;
} }
if ($this->nmo->to_user instanceof ClientContact) { if ($this->nmo->to_user instanceof ClientContact)
$this->logMailError($message, $this->nmo->to_user->client); $this->logMailError($message, $this->nmo->to_user->client);
} }
}
private function setMailDriver() private function setMailDriver()
{ {
@ -188,6 +191,7 @@ class NinjaMailerJob implements ShouldQueue
default: default:
break; break;
} }
} }
private function setOfficeMailer() private function setOfficeMailer()
@ -196,23 +200,35 @@ class NinjaMailerJob implements ShouldQueue
$user = User::find($this->decodePrimaryKey($sending_user)); $user = User::find($this->decodePrimaryKey($sending_user));
/* Always ensure the user is set on the correct account */
if($user->account_id != $this->company->account_id){
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
nlog("Sending via {$user->name()}"); nlog("Sending via {$user->name()}");
$token = $this->refreshOfficeToken($user); $token = $this->refreshOfficeToken($user);
if ($token) { if($token)
{
$user->oauth_user_token = $token; $user->oauth_user_token = $token;
$user->save(); $user->save();
} else {
$this->nmo->settings->email_sending_method = 'default';
}
else {
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver(); return $this->setMailDriver();
} }
$this->nmo $this->nmo
->mailable ->mailable
->from($user->email, $user->name()) ->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) { ->withSwiftMessage(function ($message) use($token) {
$message->getHeaders()->addTextHeader('GmailToken', $token); $message->getHeaders()->addTextHeader('GmailToken', $token);
}); });
@ -221,19 +237,27 @@ class NinjaMailerJob implements ShouldQueue
private function setGmailMailer() private function setGmailMailer()
{ {
if (LaravelGmail::check()) { if(LaravelGmail::check())
LaravelGmail::logout(); LaravelGmail::logout();
}
$sending_user = $this->nmo->settings->gmail_sending_user_id; $sending_user = $this->nmo->settings->gmail_sending_user_id;
$user = User::find($this->decodePrimaryKey($sending_user)); $user = User::find($this->decodePrimaryKey($sending_user));
nlog("Gmail sending via {$user->name()}"); /* Always ensure the user is set on the correct account */
if($user->account_id != $this->company->account_id){
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
nlog("Sending via {$user->name()}");
$google = (new Google())->init(); $google = (new Google())->init();
try{ try{
if ($google->getClient()->isAccessTokenExpired()) { if ($google->getClient()->isAccessTokenExpired()) {
$google->refreshToken($user); $google->refreshToken($user);
$user = $user->fresh(); $user = $user->fresh();
@ -242,20 +266,20 @@ class NinjaMailerJob implements ShouldQueue
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token)); $google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
sleep(rand(2,6)); sleep(rand(2,6));
} catch (\Exception $e) { }
catch(\Exception $e) {
$this->logMailError('Gmail Token Invalid', $this->company->clients()->first()); $this->logMailError('Gmail Token Invalid', $this->company->clients()->first());
$this->nmo->settings->email_sending_method = 'default'; $this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver(); return $this->setMailDriver();
} }
/** /**
* If the user doesn't have a valid token, notify them * If the user doesn't have a valid token, notify them
*/ */
if(!$user->oauth_user_token) { if(!$user->oauth_user_token) {
$this->company->account->gmailCredentialNotification(); $this->company->account->gmailCredentialNotification();
$this->nmo->settings->email_sending_method = 'default'; $this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver(); return $this->setMailDriver();
} }
@ -270,50 +294,55 @@ class NinjaMailerJob implements ShouldQueue
if(!$token) { if(!$token) {
$this->company->account->gmailCredentialNotification(); $this->company->account->gmailCredentialNotification();
$this->nmo->settings->email_sending_method = 'default'; $this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver(); return $this->setMailDriver();
} }
$this->nmo $this->nmo
->mailable ->mailable
->from($user->email, $user->name()) ->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) { ->withSwiftMessage(function ($message) use($token) {
$message->getHeaders()->addTextHeader('GmailToken', $token); $message->getHeaders()->addTextHeader('GmailToken', $token);
}); });
} }
private function preFlightChecksFail() private function preFlightChecksFail()
{ {
/* If we are migrating data we don't want to fire any emails */ /* If we are migrating data we don't want to fire any emails */
if ($this->nmo->company->is_disabled && ! $this->override) { if($this->company->is_disabled && !$this->override)
return true; 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 */ /* 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() && $this->nmo->to_user && 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; return true;
}
/* GMail users are uncapped */ /* GMail users are uncapped */
if (Ninja::isHosted() && $this->nmo->settings->email_sending_method == 'gmail') { if(Ninja::isHosted() && ($this->nmo->settings->email_sending_method == 'gmail' || $this->nmo->settings->email_sending_method == 'office365'))
return false; return false;
}
/* On the hosted platform, if the user is over the email quotas, we do not send the email. */ /* On the hosted platform, if the user is over the email quotas, we do not send the email. */
if (Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded()) { if(Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded())
return true;
/* To handle spam users we drop all emails from flagged accounts */
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_flagged)
return true; return true;
}
/* Ensure the user has a valid email address */ /* Ensure the user has a valid email address */
if (! str_contains($this->nmo->to_user->email, '@')) { if(!str_contains($this->nmo->to_user->email, "@"))
return true; return true;
}
/* On the hosted platform we actively scan all outbound emails to ensure outbound email quality remains high */
if(class_exists(\Modules\Admin\Jobs\Account\EmailQuality::class))
return (new \Modules\Admin\Jobs\Account\EmailQuality($this->nmo, $this->company))->run();
return false; return false;
} }
private function logMailError($errors, $recipient_object) private function logMailError($errors, $recipient_object)
{ {
SystemLogger::dispatch( SystemLogger::dispatch(
$errors, $errors,
SystemLog::CATEGORY_MAIL, SystemLog::CATEGORY_MAIL,
@ -333,9 +362,14 @@ class NinjaMailerJob implements ShouldQueue
public function failed($exception = null) public function failed($exception = null)
{ {
} }
private function refreshOfficeToken($user) private function refreshOfficeToken($user)
{
$expiry = $user->oauth_user_token_expiry ?: now()->subDay();
if($expiry->lt(now()))
{ {
$guzzle = new \GuzzleHttp\Client(); $guzzle = new \GuzzleHttp\Client();
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; $url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
@ -344,16 +378,37 @@ class NinjaMailerJob implements ShouldQueue
'form_params' => [ 'form_params' => [
'client_id' => config('ninja.o365.client_id') , 'client_id' => config('ninja.o365.client_id') ,
'client_secret' => config('ninja.o365.client_secret') , 'client_secret' => config('ninja.o365.client_secret') ,
'scope' => 'email Mail.ReadWrite Mail.Send offline_access profile User.Read openid', 'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token', 'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token, 'refresh_token' => $user->oauth_user_refresh_token
], ],
])->getBody()->getContents()); ])->getBody()->getContents());
nlog($token);
if($token){ if($token){
$user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token;
$user->oauth_user_token = $token->access_token;
$user->oauth_user_token_expiry = now()->addSeconds($token->expires_in);
$user->save();
return $token->access_token; return $token->access_token;
} }
return false; return false;
} }
return $user->oauth_user_token;
}
/**
* Is this the cleanest way to requeue a job?
*
* $this->delete();
*
* $job = NinjaMailerJob::dispatch($this->nmo, $this->override)->delay(3600);
*/
} }

View File

@ -49,7 +49,6 @@ class ProcessPostmarkWebhook implements ShouldQueue
private array $request; private array $request;
public $invitation; public $invitation;
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -71,19 +70,19 @@ class ProcessPostmarkWebhook implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
MultiDB::findAndSetDbByCompanyKey($this->request['Tag']); MultiDB::findAndSetDbByCompanyKey($this->request['Tag']);
$this->invitation = $this->discoverInvitation($this->request['MessageID']); $this->invitation = $this->discoverInvitation($this->request['MessageID']);
if (! $this->invitation) { if(!$this->invitation)
return; return;
}
if (array_key_exists('Details', $this->request)) { if(array_key_exists('Details', $this->request))
$this->invitation->email_error = $this->request['Details']; $this->invitation->email_error = $this->request['Details'];
}
switch ($this->request['RecordType']) { switch ($this->request['RecordType'])
{
case 'Delivery': case 'Delivery':
return $this->processDelivery(); return $this->processDelivery();
case 'Bounce': case 'Bounce':
@ -93,9 +92,10 @@ class ProcessPostmarkWebhook implements ShouldQueue
case 'Open': case 'Open':
return $this->processOpen(); return $this->processOpen();
default: default:
// code... # code...
break; break;
} }
} }
// { // {
@ -137,6 +137,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
private function processOpen() private function processOpen()
{ {
$this->invitation->opened_date = now(); $this->invitation->opened_date = now();
$this->invitation->save(); $this->invitation->save();
@ -147,6 +148,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
$this->invitation->contact->client, $this->invitation->contact->client,
$this->invitation->company $this->invitation->company
); );
} }
// { // {
@ -218,9 +220,9 @@ class ProcessPostmarkWebhook implements ShouldQueue
SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company);
if (config('ninja.notification.slack')) { // if(config('ninja.notification.slack'))
$this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja(); // $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja();
}
} }
// { // {
@ -250,6 +252,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
// } // }
private function processSpamComplaint() private function processSpamComplaint()
{ {
$this->invitation->email_status = 'spam'; $this->invitation->email_status = 'spam';
$this->invitation->save(); $this->invitation->save();
@ -263,25 +266,24 @@ class ProcessPostmarkWebhook implements ShouldQueue
SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company);
if (config('ninja.notification.slack')) { if(config('ninja.notification.slack'))
$this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja(); $this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja();
}
} }
private function discoverInvitation($message_id) private function discoverInvitation($message_id)
{ {
$invitation = false; $invitation = false;
if ($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) { if($invitation = InvoiceInvitation::where('message_id', $message_id)->first())
return $invitation; return $invitation;
} elseif ($invitation = QuoteInvitation::where('message_id', $message_id)->first()) { elseif($invitation = QuoteInvitation::where('message_id', $message_id)->first())
return $invitation; return $invitation;
} elseif ($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) { elseif($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first())
return $invitation; return $invitation;
} elseif ($invitation = CreditInvitation::where('message_id', $message_id)->first()) { elseif($invitation = CreditInvitation::where('message_id', $message_id)->first())
return $invitation; return $invitation;
} else { else
return $invitation; return $invitation;
} }
} }
}

View File

@ -81,7 +81,6 @@ class SendRecurring implements ShouldQueue
$invoice = $invoice->service() $invoice = $invoice->service()
->markSent() ->markSent()
->applyNumber() ->applyNumber()
//->createInvitations() //need to only link invitations to those in the recurring invoice
->fillDefaults() ->fillDefaults()
->adjustInventory() ->adjustInventory()
->save(); ->save();

View File

@ -41,8 +41,8 @@ use App\Jobs\Ninja\CheckCompanyData;
use App\Jobs\Ninja\CompanySizeCheck; use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Util\VersionCheck; use App\Jobs\Util\VersionCheck;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Mail\Migration\StripeConnectMigration;
use App\Mail\MigrationCompleted; use App\Mail\MigrationCompleted;
use App\Mail\Migration\StripeConnectMigration;
use App\Models\Activity; use App\Models\Activity;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
@ -106,7 +106,6 @@ class Import implements ShouldQueue
use CleanLineItems; use CleanLineItems;
use Uploadable; use Uploadable;
use SavesDocuments; use SavesDocuments;
/** /**
* @var array * @var array
*/ */
@ -191,7 +190,7 @@ class Import implements ShouldQueue
public function middleware() public function middleware()
{ {
return [new WithoutOverlapping($this->company->account->key)]; return [new WithoutOverlapping($this->company->company_key)];
} }
/** /**
@ -203,9 +202,9 @@ class Import implements ShouldQueue
{ {
set_time_limit(0); set_time_limit(0);
nlog('Starting Migration'); nlog("Starting Migration");
nlog($this->user->email); nlog($this->user->email);
nlog('Company ID = '); nlog("Company ID = ");
nlog($this->company->id); nlog($this->company->id);
auth()->login($this->user, false); auth()->login($this->user, false);
@ -253,7 +252,7 @@ class Import implements ShouldQueue
$this->setInitialCompanyLedgerBalances(); $this->setInitialCompanyLedgerBalances();
// $this->fixClientBalances(); // $this->fixClientBalances();
$check_data = (new CheckCompanyData($this->company, md5(time())))->handle(); $check_data = CheckCompanyData::dispatchNow($this->company, md5(time()));
// if(Ninja::isHosted() && array_key_exists('ninja_tokens', $data)) // if(Ninja::isHosted() && array_key_exists('ninja_tokens', $data))
$this->processNinjaTokens($data['ninja_tokens']); $this->processNinjaTokens($data['ninja_tokens']);
@ -265,8 +264,9 @@ class Import implements ShouldQueue
$t->replace(Ninja::transformTranslations($this->company->settings)); $t->replace(Ninja::transformTranslations($this->company->settings));
Mail::to($this->user->email, $this->user->name()) Mail::to($this->user->email, $this->user->name())
->send(new MigrationCompleted($this->company, implode('<br>', $check_data))); ->send(new MigrationCompleted($this->company, implode("<br>",$check_data)));
} catch (\Exception $e) { }
catch(\Exception $e) {
nlog($e->getMessage()); nlog($e->getMessage());
} }
@ -275,16 +275,23 @@ class Import implements ShouldQueue
info('Completed🚀🚀🚀🚀🚀 at '.now()); info('Completed🚀🚀🚀🚀🚀 at '.now());
try{
unlink($this->file_path); unlink($this->file_path);
} }
catch(\Exception $e){
nlog("problem unsetting file");
}
}
private function fixData() private function fixData()
{ {
$this->company->clients()->withTrashed()->where('is_deleted', 0)->cursor()->each(function ($client) { $this->company->clients()->withTrashed()->where('is_deleted', 0)->cursor()->each(function ($client) {
$total_invoice_payments = 0; $total_invoice_payments = 0;
$credit_total_applied = 0; $credit_total_applied = 0;
foreach ($client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get() as $invoice) { foreach ($client->invoices()->where('is_deleted', false)->where('status_id', '>', 1)->get() as $invoice) {
$total_amount = $invoice->payments()->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->get()->sum('pivot.amount'); $total_amount = $invoice->payments()->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->get()->sum('pivot.amount');
$total_refund = $invoice->payments()->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->get()->sum('pivot.refunded'); $total_refund = $invoice->payments()->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->get()->sum('pivot.refunded');
@ -300,16 +307,21 @@ class Import implements ShouldQueue
$total_invoice_payments += $credit_total_applied; $total_invoice_payments += $credit_total_applied;
} }
if (round($total_invoice_payments, 2) != round($client->paid_to_date, 2)) { if (round($total_invoice_payments, 2) != round($client->paid_to_date, 2)) {
$client->paid_to_date = $total_invoice_payments; $client->paid_to_date = $total_invoice_payments;
$client->save(); $client->save();
} }
}); });
} }
private function setInitialCompanyLedgerBalances() private function setInitialCompanyLedgerBalances()
{ {
Client::where('company_id', $this->company->id)->cursor()->each(function ($client) { Client::where('company_id', $this->company->id)->cursor()->each(function ($client) {
$invoice_balances = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance'); $invoice_balances = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$company_ledger = CompanyLedgerFactory::create($client->company_id, $client->user_id); $company_ledger = CompanyLedgerFactory::create($client->company_id, $client->user_id);
@ -324,6 +336,7 @@ class Import implements ShouldQueue
$client->balance = $invoice_balances; $client->balance = $invoice_balances;
$client->save(); $client->save();
}); });
} }
@ -355,7 +368,7 @@ class Import implements ShouldQueue
if ( if (
$data['settings']['invoice_design_id'] > 9 || $data['settings']['invoice_design_id'] > 9 ||
$data['settings']['invoice_design_id'] > '9' $data['settings']['invoice_design_id'] > "9"
) { ) {
$data['settings']['invoice_design_id'] = 1; $data['settings']['invoice_design_id'] = 1;
} }
@ -364,30 +377,27 @@ class Import implements ShouldQueue
$data = $this->transformCompanyData($data); $data = $this->transformCompanyData($data);
if(Ninja::isHosted()) { if(Ninja::isHosted()) {
if (!MultiDB::checkDomainAvailable($data['subdomain'])) {
$data['subdomain'] = MultiDB::randomSubdomainGenerator();
}
if (strlen($data['subdomain']) == 0) { if(!MultiDB::checkDomainAvailable($data['subdomain']))
$data['subdomain'] = MultiDB::randomSubdomainGenerator(); $data['subdomain'] = MultiDB::randomSubdomainGenerator();
}
if(strlen($data['subdomain']) == 0)
$data['subdomain'] = MultiDB::randomSubdomainGenerator();
} }
$rules = (new UpdateCompanyRequest())->rules(); $rules = (new UpdateCompanyRequest())->rules();
$validator = Validator::make($data, $rules); $validator = Validator::make($data, $rules);
if ($validator->fails()) { if ($validator->fails())
throw new MigrationValidatorFailed(json_encode($validator->errors())); throw new MigrationValidatorFailed(json_encode($validator->errors()));
}
if (isset($data['account_id'])) { if (isset($data['account_id']))
unset($data['account_id']); unset($data['account_id']);
}
if (isset($data['version'])) { if(isset($data['version']))
unset($data['version']); unset($data['version']);
}
if (isset($data['referral_code'])) { if (isset($data['referral_code'])) {
$account = $this->company->account; $account = $this->company->account;
@ -405,15 +415,18 @@ class Import implements ShouldQueue
$company_repository->save($data, $this->company); $company_repository->save($data, $this->company);
if (isset($data['settings']->company_logo) && strlen($data['settings']->company_logo) > 0) { if (isset($data['settings']->company_logo) && strlen($data['settings']->company_logo) > 0) {
try { try {
$tempImage = tempnam(sys_get_temp_dir(), basename($data['settings']->company_logo)); $tempImage = tempnam(sys_get_temp_dir(), basename($data['settings']->company_logo));
copy($data['settings']->company_logo, $tempImage); copy($data['settings']->company_logo, $tempImage);
$this->uploadLogo($tempImage, $this->company, $this->company); $this->uploadLogo($tempImage, $this->company, $this->company);
} catch (\Exception $e) { } catch (\Exception $e) {
$settings = $this->company->settings; $settings = $this->company->settings;
$settings->company_logo = ''; $settings->company_logo = '';
$this->company->settings = $settings; $this->company->settings = $settings;
$this->company->save(); $this->company->save();
} }
} }
@ -428,29 +441,24 @@ class Import implements ShouldQueue
private function parseCustomFields($fields) :array private function parseCustomFields($fields) :array
{ {
if (array_key_exists('account1', $fields)) {
if(array_key_exists('account1', $fields))
$fields['company1'] = $fields['account1']; $fields['company1'] = $fields['account1'];
}
if (array_key_exists('account2', $fields)) { if(array_key_exists('account2', $fields))
$fields['company2'] = $fields['account2']; $fields['company2'] = $fields['account2'];
}
if (array_key_exists('invoice1', $fields)) { if(array_key_exists('invoice1', $fields))
$fields['surcharge1'] = $fields['invoice1']; $fields['surcharge1'] = $fields['invoice1'];
}
if (array_key_exists('invoice2', $fields)) { if(array_key_exists('invoice2', $fields))
$fields['surcharge2'] = $fields['invoice2']; $fields['surcharge2'] = $fields['invoice2'];
}
if (array_key_exists('invoice_text1', $fields)) { if(array_key_exists('invoice_text1', $fields))
$fields['invoice1'] = $fields['invoice_text1']; $fields['invoice1'] = $fields['invoice_text1'];
}
if (array_key_exists('invoice_text2', $fields)) { if(array_key_exists('invoice_text2', $fields))
$fields['invoice2'] = $fields['invoice_text2']; $fields['invoice2'] = $fields['invoice_text2'];
}
foreach ($fields as &$value) { foreach ($fields as &$value) {
$value = (string) $value; $value = (string) $value;
@ -461,6 +469,7 @@ class Import implements ShouldQueue
private function transformCompanyData(array $data): array private function transformCompanyData(array $data): array
{ {
$company_settings = CompanySettings::defaults(); $company_settings = CompanySettings::defaults();
if (array_key_exists('settings', $data)) { if (array_key_exists('settings', $data)) {
@ -468,14 +477,14 @@ class Import implements ShouldQueue
if ($key == 'invoice_design_id' || $key == 'quote_design_id' || $key == 'credit_design_id') { if ($key == 'invoice_design_id' || $key == 'quote_design_id' || $key == 'credit_design_id') {
$value = $this->encodePrimaryKey($value); $value = $this->encodePrimaryKey($value);
if (!$value) { if(!$value)
$value = $this->encodePrimaryKey(1); $value = $this->encodePrimaryKey(1);
}
} }
/* changes $key = '' to $value == '' and changed the return value from -1 to "0" 06/01/2022 */ /* changes $key = '' to $value == '' and changed the return value from -1 to "0" 06/01/2022 */
if ($key == 'payment_terms' && $value == '') { if ($key == 'payment_terms' && $value == '') {
$value = '0'; $value = "0";
} }
$company_settings->{$key} = $value; $company_settings->{$key} = $value;
@ -483,11 +492,13 @@ class Import implements ShouldQueue
if($key == 'payment_terms'){ if($key == 'payment_terms'){
settype($company_settings->payment_terms, 'string'); settype($company_settings->payment_terms, 'string');
} }
} }
$data['settings'] = $company_settings; $data['settings'] = $company_settings;
} }
return $data; return $data;
} }
@ -573,15 +584,14 @@ class Import implements ShouldQueue
$user->email_verified_at = now(); $user->email_verified_at = now();
// $user->confirmation_code = ''; // $user->confirmation_code = '';
if ($modified['deleted_at']) { if($modified['deleted_at'])
$user->deleted_at = now(); $user->deleted_at = now();
}
$user->save(); $user->save();
$user_agent = array_key_exists('token_name', $resource) ?: request()->server('HTTP_USER_AGENT'); $user_agent = array_key_exists('token_name', $resource) ?: request()->server('HTTP_USER_AGENT');
CreateCompanyToken::dispatchSync($this->company, $user, $user_agent); CreateCompanyToken::dispatchNow($this->company, $user, $user_agent);
$key = "users_{$resource['id']}"; $key = "users_{$resource['id']}";
@ -609,9 +619,8 @@ class Import implements ShouldQueue
->withTrashed() ->withTrashed()
->exists(); ->exists();
if ($model_query) { if($model_query)
return $value . '_' . Str::random(5); return $value . '_' . Str::random(5);
}
return $value; return $value;
} }
@ -646,13 +655,11 @@ class Import implements ShouldQueue
) )
); );
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$client->created_at = Carbon::parse($modified['created_at']); $client->created_at = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$client->updated_at = Carbon::parse($modified['updated_at']); $client->updated_at = Carbon::parse($modified['updated_at']);
}
$client->country_id = array_key_exists('country_id', $modified) ? $modified['country_id'] : $this->company->settings->country_id; $client->country_id = array_key_exists('country_id', $modified) ? $modified['country_id'] : $this->company->settings->country_id;
$client->save(['timestamps' => false]); $client->save(['timestamps' => false]);
@ -678,6 +685,7 @@ class Import implements ShouldQueue
//link contact ids //link contact ids
foreach ($resource['contacts'] as $key => $old_contact) { foreach ($resource['contacts'] as $key => $old_contact) {
$contact_match = ClientContact::where('contact_key', $old_contact['contact_key']) $contact_match = ClientContact::where('contact_key', $old_contact['contact_key'])
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('client_id', $client->id) ->where('client_id', $client->id)
@ -685,10 +693,12 @@ class Import implements ShouldQueue
->first(); ->first();
if ($contact_match) { if ($contact_match) {
$this->ids['client_contacts']['client_contacts_'.$old_contact['id']] = [ $this->ids['client_contacts']['client_contacts_'.$old_contact['id']] = [
'old' => $old_contact['id'], 'old' => $old_contact['id'],
'new' => $contact_match->id, 'new' => $contact_match->id,
]; ];
} }
} }
} }
@ -731,13 +741,11 @@ class Import implements ShouldQueue
unset($modified['id']); unset($modified['id']);
unset($modified['contacts']); unset($modified['contacts']);
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$modified['created_at'] = Carbon::parse($modified['created_at']); $modified['created_at'] = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$modified['updated_at'] = Carbon::parse($modified['updated_at']); $modified['updated_at'] = Carbon::parse($modified['updated_at']);
}
$vendor = $vendor_repository->save( $vendor = $vendor_repository->save(
$modified, $modified,
@ -781,6 +789,7 @@ class Import implements ShouldQueue
$client_repository = null; $client_repository = null;
} }
private function processProducts(array $data): void private function processProducts(array $data): void
{ {
Product::unguard(); Product::unguard();
@ -805,13 +814,11 @@ class Import implements ShouldQueue
$modified['company_id'] = $this->company->id; $modified['company_id'] = $this->company->id;
$modified['user_id'] = $this->processUserId($resource); $modified['user_id'] = $this->processUserId($resource);
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$modified['created_at'] = Carbon::parse($modified['created_at']); $modified['created_at'] = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$modified['updated_at'] = Carbon::parse($modified['updated_at']); $modified['updated_at'] = Carbon::parse($modified['updated_at']);
}
unset($modified['id']); unset($modified['id']);
@ -867,13 +874,11 @@ class Import implements ShouldQueue
$expense = RecurringExpense::create($modified); $expense = RecurringExpense::create($modified);
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$expense->created_at = Carbon::parse($modified['created_at']); $expense->created_at = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$expense->updated_at = Carbon::parse($modified['updated_at']); $expense->updated_at = Carbon::parse($modified['updated_at']);
}
$expense->save(['timestamps' => false]); $expense->save(['timestamps' => false]);
@ -885,8 +890,10 @@ class Import implements ShouldQueue
'old' => $resource['id'], 'old' => $resource['id'],
'new' => $expense->id, 'new' => $expense->id,
]; ];
} }
RecurringExpense::reguard(); RecurringExpense::reguard();
/*Improve memory handling by setting everything to null when we have finished*/ /*Improve memory handling by setting everything to null when we have finished*/
@ -921,13 +928,14 @@ class Import implements ShouldQueue
$modified['company_id'] = $this->company->id; $modified['company_id'] = $this->company->id;
$modified['line_items'] = $this->cleanItems($modified['line_items']); $modified['line_items'] = $this->cleanItems($modified['line_items']);
if (array_key_exists('created_at', $modified)) { if(array_key_exists('next_send_date', $resource))
$modified['created_at'] = Carbon::parse($modified['created_at']); $modified['next_send_date_client'] = $resource['next_send_date'];
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('created_at', $modified))
$modified['created_at'] = Carbon::parse($modified['created_at']);
if(array_key_exists('updated_at', $modified))
$modified['updated_at'] = Carbon::parse($modified['updated_at']); $modified['updated_at'] = Carbon::parse($modified['updated_at']);
}
unset($modified['id']); unset($modified['id']);
@ -940,9 +948,11 @@ class Import implements ShouldQueue
unset($resource['invitations'][$key]['recurring_invoice_id']); unset($resource['invitations'][$key]['recurring_invoice_id']);
unset($resource['invitations'][$key]['id']); unset($resource['invitations'][$key]['id']);
} }
$modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']); $modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']);
} }
$invoice = $invoice_repository->save( $invoice = $invoice_repository->save(
@ -995,9 +1005,8 @@ class Import implements ShouldQueue
$modified['client_id'] = $this->transformId('clients', $resource['client_id']); $modified['client_id'] = $this->transformId('clients', $resource['client_id']);
if (array_key_exists('recurring_id', $resource) && !is_null($resource['recurring_id'])) { if(array_key_exists('recurring_id', $resource) && !is_null($resource['recurring_id']))
$modified['recurring_id'] = $this->transformId('recurring_invoices', (string)$resource['recurring_id']); $modified['recurring_id'] = $this->transformId('recurring_invoices', (string)$resource['recurring_id']);
}
$modified['user_id'] = $this->processUserId($resource); $modified['user_id'] = $this->processUserId($resource);
$modified['company_id'] = $this->company->id; $modified['company_id'] = $this->company->id;
@ -1016,6 +1025,7 @@ class Import implements ShouldQueue
} }
$modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']); $modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']);
} }
$invoice = $invoice_repository->save( $invoice = $invoice_repository->save(
@ -1038,6 +1048,7 @@ class Import implements ShouldQueue
$invoice_repository = null; $invoice_repository = null;
} }
/* Prevent edge case where V4 has inserted multiple invitations for a resource for a client contact */ /* Prevent edge case where V4 has inserted multiple invitations for a resource for a client contact */
private function deDuplicateInvitations($invitations) private function deDuplicateInvitations($invitations)
{ {
@ -1071,13 +1082,11 @@ class Import implements ShouldQueue
$modified['user_id'] = $this->processUserId($resource); $modified['user_id'] = $this->processUserId($resource);
$modified['company_id'] = $this->company->id; $modified['company_id'] = $this->company->id;
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$modified['created_at'] = Carbon::parse($modified['created_at']); $modified['created_at'] = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$modified['updated_at'] = Carbon::parse($modified['updated_at']); $modified['updated_at'] = Carbon::parse($modified['updated_at']);
}
unset($modified['id']); unset($modified['id']);
@ -1093,6 +1102,7 @@ class Import implements ShouldQueue
$client->save(); $client->save();
} }
$key = "credits_{$resource['id']}"; $key = "credits_{$resource['id']}";
$this->ids['credits'][$key] = [ $this->ids['credits'][$key] = [
@ -1106,6 +1116,7 @@ class Import implements ShouldQueue
/*Improve memory handling by setting everything to null when we have finished*/ /*Improve memory handling by setting everything to null when we have finished*/
$data = null; $data = null;
$credit_repository = null; $credit_repository = null;
} }
private function processQuotes(array $data): void private function processQuotes(array $data): void
@ -1133,32 +1144,28 @@ class Import implements ShouldQueue
$modified['client_id'] = $this->transformId('clients', $resource['client_id']); $modified['client_id'] = $this->transformId('clients', $resource['client_id']);
if (array_key_exists('invoice_id', $resource) && isset($resource['invoice_id']) && $this->tryTransformingId('invoices', $resource['invoice_id'])) { if(array_key_exists('invoice_id', $resource) && isset($resource['invoice_id']) && $this->tryTransformingId('invoices', $resource['invoice_id']))
$modified['invoice_id'] = $this->transformId('invoices', $resource['invoice_id']); $modified['invoice_id'] = $this->transformId('invoices', $resource['invoice_id']);
}
$modified['user_id'] = $this->processUserId($resource); $modified['user_id'] = $this->processUserId($resource);
$modified['company_id'] = $this->company->id; $modified['company_id'] = $this->company->id;
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$modified['created_at'] = Carbon::parse($modified['created_at']); $modified['created_at'] = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$modified['updated_at'] = Carbon::parse($modified['updated_at']); $modified['updated_at'] = Carbon::parse($modified['updated_at']);
}
if (array_key_exists('tax_rate1', $modified) && is_null($modified['tax_rate1'])) { if(array_key_exists('tax_rate1', $modified) && is_null($modified['tax_rate1']))
$modified['tax_rate1'] = 0; $modified['tax_rate1'] = 0;
}
if (array_key_exists('tax_rate2', $modified) && is_null($modified['tax_rate2'])) { if(array_key_exists('tax_rate2', $modified) && is_null($modified['tax_rate2']))
$modified['tax_rate2'] = 0; $modified['tax_rate2'] = 0;
}
unset($modified['id']); unset($modified['id']);
if (array_key_exists('invitations', $resource)) { if (array_key_exists('invitations', $resource)) {
foreach ($resource['invitations'] as $key => $invite) { foreach ($resource['invitations'] as $key => $invite) {
$resource['invitations'][$key]['client_contact_id'] = $this->transformId('client_contacts', $invite['client_contact_id']); $resource['invitations'][$key]['client_contact_id'] = $this->transformId('client_contacts', $invite['client_contact_id']);
@ -1170,6 +1177,7 @@ class Import implements ShouldQueue
} }
$modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']); $modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']);
} }
$quote = $quote_repository->save( $quote = $quote_repository->save(
@ -1177,13 +1185,11 @@ class Import implements ShouldQueue
QuoteFactory::create($this->company->id, $modified['user_id']) QuoteFactory::create($this->company->id, $modified['user_id'])
); );
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$quote->created_at = $modified['created_at']; $quote->created_at = $modified['created_at'];
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$quote->updated_at = $modified['updated_at']; $quote->updated_at = $modified['updated_at'];
}
$quote->save(['timestamps' => false]); $quote->save(['timestamps' => false]);
@ -1251,24 +1257,23 @@ class Import implements ShouldQueue
PaymentFactory::create($this->company->id, $modified['user_id']) PaymentFactory::create($this->company->id, $modified['user_id'])
); );
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$payment->created_at = Carbon::parse($modified['created_at']); $payment->created_at = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$payment->updated_at = Carbon::parse($modified['updated_at']); $payment->updated_at = Carbon::parse($modified['updated_at']);
}
$payment->save(['timestamps' => false]); $payment->save(['timestamps' => false]);
if (array_key_exists('company_gateway_id', $resource) && isset($resource['company_gateway_id']) && $resource['company_gateway_id'] != 'NULL') { if (array_key_exists('company_gateway_id', $resource) && isset($resource['company_gateway_id']) && $resource['company_gateway_id'] != 'NULL') {
if ($this->tryTransformingId('company_gateways', $resource['company_gateway_id'])) {
if($this->tryTransformingId('company_gateways', $resource['company_gateway_id']))
$payment->company_gateway_id = $this->transformId('company_gateways', $resource['company_gateway_id']); $payment->company_gateway_id = $this->transformId('company_gateways', $resource['company_gateway_id']);
}
$payment->save(); $payment->save();
} }
$old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id; $old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id;
$this->ids['payments'] = [ $this->ids['payments'] = [
@ -1281,6 +1286,8 @@ class Import implements ShouldQueue
if(in_array($payment->status_id, [Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED])) { if(in_array($payment->status_id, [Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED])) {
$this->processPaymentRefund($payment); $this->processPaymentRefund($payment);
} }
} }
Payment::reguard(); Payment::reguard();
@ -1295,13 +1302,16 @@ class Import implements ShouldQueue
$invoices = $payment->invoices()->get(); $invoices = $payment->invoices()->get();
$invoices->each(function ($invoice) use($payment) { $invoices->each(function ($invoice) use($payment) {
if ($payment->refunded > 0 && in_array($invoice->status_id, [Invoice::STATUS_SENT])) { if ($payment->refunded > 0 && in_array($invoice->status_id, [Invoice::STATUS_SENT])) {
$invoice->service() $invoice->service()
->updateBalance($payment->refunded) ->updateBalance($payment->refunded)
->updatePaidToDate($payment->refunded*-1) ->updatePaidToDate($payment->refunded*-1)
->updateStatus() ->updateStatus()
->save(); ->save();
} }
}); });
} }
@ -1330,13 +1340,11 @@ class Import implements ShouldQueue
case 5: case 5:
$payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED; $payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED;
$payment->save(); $payment->save();
return $payment; return $payment;
break; break;
case 6: case 6:
$payment->status_id = Payment::STATUS_REFUNDED; $payment->status_id = Payment::STATUS_REFUNDED;
$payment->save(); $payment->save();
return $payment; return $payment;
break; break;
@ -1352,6 +1360,7 @@ class Import implements ShouldQueue
/* No validators since data provided by database is already valid. */ /* No validators since data provided by database is already valid. */
foreach ($data as $resource) { foreach ($data as $resource) {
$modified = $resource; $modified = $resource;
if (array_key_exists('invoice_id', $resource) && $resource['invoice_id'] && ! array_key_exists('invoices', $this->ids)) { if (array_key_exists('invoice_id', $resource) && $resource['invoice_id'] && ! array_key_exists('invoices', $this->ids)) {
@ -1365,6 +1374,7 @@ class Import implements ShouldQueue
} }
if (array_key_exists('invoice_id', $resource) && $resource['invoice_id'] && array_key_exists('invoices', $this->ids)) { if (array_key_exists('invoice_id', $resource) && $resource['invoice_id'] && array_key_exists('invoices', $this->ids)) {
$try_quote = false; $try_quote = false;
$exception = false; $exception = false;
$entity = false; $entity = false;
@ -1372,7 +1382,8 @@ class Import implements ShouldQueue
try{ try{
$invoice_id = $this->transformId('invoices', $resource['invoice_id']); $invoice_id = $this->transformId('invoices', $resource['invoice_id']);
$entity = Invoice::where('id', $invoice_id)->withTrashed()->first(); $entity = Invoice::where('id', $invoice_id)->withTrashed()->first();
} catch (\Exception $e) { }
catch(\Exception $e){
nlog("i couldn't find the invoice document {$resource['invoice_id']}, perhaps it is a quote?"); nlog("i couldn't find the invoice document {$resource['invoice_id']}, perhaps it is a quote?");
nlog($e->getMessage()); nlog($e->getMessage());
@ -1380,21 +1391,25 @@ class Import implements ShouldQueue
} }
if($try_quote && array_key_exists('quotes', $this->ids) ) { if($try_quote && array_key_exists('quotes', $this->ids) ) {
try{ try{
$quote_id = $this->transformId('quotes', $resource['invoice_id']); $quote_id = $this->transformId('quotes', $resource['invoice_id']);
$entity = Quote::where('id', $quote_id)->withTrashed()->first(); $entity = Quote::where('id', $quote_id)->withTrashed()->first();
} catch (\Exception $e) { }
catch(\Exception $e){
nlog("i couldn't find the quote document {$resource['invoice_id']}, perhaps it is a quote?"); nlog("i couldn't find the quote document {$resource['invoice_id']}, perhaps it is a quote?");
nlog($e->getMessage()); nlog($e->getMessage());
} }
} }
if (!$entity) { if(!$entity)
continue; continue;
}
// throw new Exception("Resource invoice/quote document not available."); // throw new Exception("Resource invoice/quote document not available.");
} }
if (array_key_exists('expense_id', $resource) && $resource['expense_id'] && array_key_exists('expenses', $this->ids)) { if (array_key_exists('expense_id', $resource) && $resource['expense_id'] && array_key_exists('expenses', $this->ids)) {
$expense_id = $this->transformId('expenses', $resource['expense_id']); $expense_id = $this->transformId('expenses', $resource['expense_id']);
$entity = Expense::where('id', $expense_id)->withTrashed()->first(); $entity = Expense::where('id', $expense_id)->withTrashed()->first();
@ -1419,11 +1434,15 @@ class Import implements ShouldQueue
); );
$this->saveDocument($uploaded_file, $entity, $is_public = true); $this->saveDocument($uploaded_file, $entity, $is_public = true);
} catch (\Exception $e) { }
catch(\Exception $e) {
//do nothing, gracefully :) //do nothing, gracefully :)
} }
} }
} }
private function processPaymentTerms(array $data) :void private function processPaymentTerms(array $data) :void
@ -1477,12 +1496,12 @@ class Import implements ShouldQueue
$modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']); $modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']);
} }
if (!array_key_exists('accepted_credit_cards', $modified) || (array_key_exists('accepted_credit_cards', $modified) && empty($modified['accepted_credit_cards']))) { if(!array_key_exists('accepted_credit_cards', $modified) || (array_key_exists('accepted_credit_cards', $modified) && empty($modified['accepted_credit_cards'])))
$modified['accepted_credit_cards'] = 0; $modified['accepted_credit_cards'] = 0;
}
// /* On Hosted platform we need to advise Stripe users to connect with Stripe Connect */ // /* On Hosted platform we need to advise Stripe users to connect with Stripe Connect */
if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){ if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
$nmo = new NinjaMailerObject; $nmo = new NinjaMailerObject;
$nmo->mailable = new StripeConnectMigration($this->company); $nmo->mailable = new StripeConnectMigration($this->company);
$nmo->company = $this->company; $nmo->company = $this->company;
@ -1491,12 +1510,16 @@ class Import implements ShouldQueue
NinjaMailerJob::dispatch($nmo, true); NinjaMailerJob::dispatch($nmo, true);
$modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34'; $modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34';
} }
if(Ninja::isSelfHost() && $modified['gateway_key'] == 'd14dd26a47cecc30fdd65700bfb67b34'){ if(Ninja::isSelfHost() && $modified['gateway_key'] == 'd14dd26a47cecc30fdd65700bfb67b34'){
$modified['gateway_key'] = 'd14dd26a37cecc30fdd65700bfb55b23'; $modified['gateway_key'] = 'd14dd26a37cecc30fdd65700bfb55b23';
} }
$company_gateway = CompanyGateway::create($modified); $company_gateway = CompanyGateway::create($modified);
$key = "company_gateways_{$resource['id']}"; $key = "company_gateways_{$resource['id']}";
@ -1595,6 +1618,7 @@ class Import implements ShouldQueue
'old' => $resource['id'], 'old' => $resource['id'],
'new' => $expense_category->id, 'new' => $expense_category->id,
]; ];
} }
ExpenseCategory::reguard(); ExpenseCategory::reguard();
@ -1632,13 +1656,13 @@ class Import implements ShouldQueue
$task = Task::Create($modified); $task = Task::Create($modified);
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$task->created_at = Carbon::parse($modified['created_at']); $task->created_at = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$task->updated_at = Carbon::parse($modified['updated_at']); $task->updated_at = Carbon::parse($modified['updated_at']);
}
$task->save(['timestamps' => false]); $task->save(['timestamps' => false]);
@ -1722,13 +1746,13 @@ class Import implements ShouldQueue
$expense = Expense::Create($modified); $expense = Expense::Create($modified);
if (array_key_exists('created_at', $modified)) { if(array_key_exists('created_at', $modified))
$expense->created_at = Carbon::parse($modified['created_at']); $expense->created_at = Carbon::parse($modified['created_at']);
}
if (array_key_exists('updated_at', $modified)) { if(array_key_exists('updated_at', $modified))
$expense->updated_at = Carbon::parse($modified['updated_at']); $expense->updated_at = Carbon::parse($modified['updated_at']);
}
$expense->save(['timestamps' => false]); $expense->save(['timestamps' => false]);
@ -1740,6 +1764,7 @@ class Import implements ShouldQueue
'old' => $resource['id'], 'old' => $resource['id'],
'new' => $expense->id, 'new' => $expense->id,
]; ];
} }
Expense::reguard(); Expense::reguard();
@ -1780,6 +1805,7 @@ class Import implements ShouldQueue
*/ */
public function transformId($resource, string $old): int public function transformId($resource, string $old): int
{ {
if (! array_key_exists($resource, $this->ids)) { if (! array_key_exists($resource, $this->ids)) {
info(print_r($resource, 1)); info(print_r($resource, 1));
throw new Exception("Resource {$resource} not available."); throw new Exception("Resource {$resource} not available.");
@ -1838,21 +1864,24 @@ class Import implements ShouldQueue
info(print_r($exception->getMessage(), 1)); info(print_r($exception->getMessage(), 1));
if (Ninja::isHosted()) { if(Ninja::isHosted())
app('sentry')->captureException($exception); app('sentry')->captureException($exception);
} }
}
public function curlGet($url, $headers = false) public function curlGet($url, $headers = false)
{ {
return $this->exec('GET', $url, null); return $this->exec('GET', $url, null);
} }
public function exec($method, $url, $data) public function exec($method, $url, $data)
{ {
$client = new \GuzzleHttp\Client(['headers' => [
$client = new \GuzzleHttp\Client(['headers' =>
[
'X-Ninja-Token' => $this->token, 'X-Ninja-Token' => $this->token,
], ]
]); ]);
$response = $client->request('GET', $url); $response = $client->request('GET', $url);
@ -1860,20 +1889,27 @@ class Import implements ShouldQueue
return $response->getBody(); return $response->getBody();
} }
private function processNinjaTokens(array $data) private function processNinjaTokens(array $data)
{ {
nlog('attempting to process Ninja Tokens');
nlog("attempting to process Ninja Tokens");
if(Ninja::isHosted()){ if(Ninja::isHosted()){
try {
\Modules\Admin\Jobs\Account\NinjaUser::dispatchSync($data, $this->company);
} catch (\Exception $e) { try{
\Modules\Admin\Jobs\Account\NinjaUser::dispatchNow($data, $this->company);
}
catch(\Exception $e){
nlog($e->getMessage()); nlog($e->getMessage());
} }
} }
} }
/* In V4 we use negative invoices (credits) and add then into the client balance. In V5, these sit off ledger and are applied later. /* In V4 we use negative invoices (credits) and add then into the client balance. In V5, these sit off ledger and are applied later.
This next section will check for credit balances and reduce the client balance so that the V5 balances are correct This next section will check for credit balances and reduce the client balance so that the V5 balances are correct
*/ */

View File

@ -39,7 +39,7 @@ class VersionCheck implements ShouldQueue
nlog("latest version = {$version_file}"); nlog("latest version = {$version_file}");
if ($version_file) { if (Ninja::isSelfHost() && $version_file) {
Account::whereNotNull('id')->update(['latest_version' => $version_file]); Account::whereNotNull('id')->update(['latest_version' => $version_file]);
} }

View File

@ -33,8 +33,8 @@ use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml; use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\NumberFormatter; use App\Utils\Traits\NumberFormatter;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Utils\Traits\Pdf\PDF; use App\Utils\Traits\Pdf\PDF;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Utils\Traits\Pdf\PdfMaker; use App\Utils\Traits\Pdf\PdfMaker;
use App\Utils\VendorHtmlEngine; use App\Utils\VendorHtmlEngine;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@ -65,6 +65,10 @@ class CreatePurchaseOrderPdf implements ShouldQueue
public $vendor; public $vendor;
private string $path = '';
private string $file_path = '';
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -84,10 +88,38 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$this->vendor->load('company'); $this->vendor->load('company');
$this->disk = $disk ?? config('filesystems.default'); $this->disk = $disk ?? config('filesystems.default');
} }
public function handle() public function handle()
{ {
$pdf = $this->rawPdf();
if ($pdf) {
try{
if(!Storage::disk($this->disk)->exists($this->path))
Storage::disk($this->disk)->makeDirectory($this->path, 0775);
Storage::disk($this->disk)->put($this->file_path, $pdf, 'public');
}
catch(\Exception $e)
{
throw new FilePermissionsFailure($e->getMessage());
}
}
return $this->file_path;
}
public function rawPdf()
{
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
/* Forget the singleton*/ /* Forget the singleton*/
@ -107,25 +139,24 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$entity_design_id = ''; $entity_design_id = '';
$path = $this->vendor->purchase_order_filepath($this->invitation); $this->path = $this->vendor->purchase_order_filepath($this->invitation);
$entity_design_id = 'purchase_order_design_id'; $entity_design_id = 'purchase_order_design_id';
$file_path = $path.$this->entity->numberFormatter().'.pdf'; $this->file_path = $this->path.$this->entity->numberFormatter().'.pdf';
$entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey('Wpmbk5ezJn'); $entity_design_id = $this->entity->design_id ? $this->entity->design_id : $this->decodePrimaryKey('Wpmbk5ezJn');
$design = Design::find($entity_design_id); $design = Design::find($entity_design_id);
/* Catch all in case migration doesn't pass back a valid design */ /* Catch all in case migration doesn't pass back a valid design */
if (! $design) { if(!$design)
$design = Design::find(2); $design = Design::find(2);
}
$html = new VendorHtmlEngine($this->invitation); $html = new VendorHtmlEngine($this->invitation);
if ($design->is_custom) { if ($design->is_custom) {
$options = [ $options = [
'custom_partials' => json_decode(json_encode($design->design), true), 'custom_partials' => json_decode(json_encode($design->design), true)
]; ];
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options); $template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
} else { } else {
@ -160,23 +191,28 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$pdf = null; $pdf = null;
try { try {
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){ if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, $this->company); $numbered_pdf = $this->pageNumbering($pdf, $this->company);
if ($numbered_pdf) { if($numbered_pdf)
$pdf = $numbered_pdf; $pdf = $numbered_pdf;
} }
} else { else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true)); $pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, $this->company); $numbered_pdf = $this->pageNumbering($pdf, $this->company);
if ($numbered_pdf) { if($numbered_pdf)
$pdf = $numbered_pdf; $pdf = $numbered_pdf;
} }
}
} catch (\Exception $e) { } catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1)); nlog(print_r($e->getMessage(), 1));
} }
@ -185,22 +221,14 @@ class CreatePurchaseOrderPdf implements ShouldQueue
info($maker->getCompiledHTML()); info($maker->getCompiledHTML());
} }
if ($pdf) { return $pdf;
try {
if (! Storage::disk($this->disk)->exists($path)) {
Storage::disk($this->disk)->makeDirectory($path, 0775);
}
Storage::disk($this->disk)->put($file_path, $pdf, 'public');
} catch (\Exception $e) {
throw new FilePermissionsFailure($e->getMessage());
}
}
return $file_path;
} }
public function failed($e) public function failed($e)
{ {
} }
} }

View File

@ -33,10 +33,9 @@ class Account extends BaseModel
use PresentableTrait; use PresentableTrait;
use MakesHash; use MakesHash;
private $free_plan_email_quota = 250; private $free_plan_email_quota = 50;
private $paid_plan_email_quota = 500; private $paid_plan_email_quota = 500;
/** /**
* @var string * @var string
*/ */
@ -65,74 +64,53 @@ class Account extends BaseModel
/** /**
* @var array * @var array
*/ */
protected $dates = [
'deleted_at',
'promo_expires',
'discount_expires',
// 'trial_started',
// 'plan_expires'
];
protected $casts = [ protected $casts = [
'promo_expires' => 'datetime',
'discount_expires' => 'datetime',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
'onboarding' => 'object', 'onboarding' => 'object',
'set_react_as_default_ap' => 'bool', 'set_react_as_default_ap' => 'bool'
]; ];
const PLAN_FREE = 'free'; const PLAN_FREE = 'free';
const PLAN_PRO = 'pro'; const PLAN_PRO = 'pro';
const PLAN_ENTERPRISE = 'enterprise'; const PLAN_ENTERPRISE = 'enterprise';
const PLAN_WHITE_LABEL = 'white_label'; const PLAN_WHITE_LABEL = 'white_label';
const PLAN_TERM_MONTHLY = 'month'; const PLAN_TERM_MONTHLY = 'month';
const PLAN_TERM_YEARLY = 'year'; const PLAN_TERM_YEARLY = 'year';
const FEATURE_TASKS = 'tasks'; const FEATURE_TASKS = 'tasks';
const FEATURE_EXPENSES = 'expenses'; const FEATURE_EXPENSES = 'expenses';
const FEATURE_QUOTES = 'quotes'; const FEATURE_QUOTES = 'quotes';
const FEATURE_PURCHASE_ORDERS = 'purchase_orders'; const FEATURE_PURCHASE_ORDERS = 'purchase_orders';
const FEATURE_CUSTOMIZE_INVOICE_DESIGN = 'custom_designs'; const FEATURE_CUSTOMIZE_INVOICE_DESIGN = 'custom_designs';
const FEATURE_DIFFERENT_DESIGNS = 'different_designs'; const FEATURE_DIFFERENT_DESIGNS = 'different_designs';
const FEATURE_EMAIL_TEMPLATES_REMINDERS = 'template_reminders'; const FEATURE_EMAIL_TEMPLATES_REMINDERS = 'template_reminders';
const FEATURE_INVOICE_SETTINGS = 'invoice_settings'; const FEATURE_INVOICE_SETTINGS = 'invoice_settings';
const FEATURE_CUSTOM_EMAILS = 'custom_emails'; const FEATURE_CUSTOM_EMAILS = 'custom_emails';
const FEATURE_PDF_ATTACHMENT = 'pdf_attachments'; const FEATURE_PDF_ATTACHMENT = 'pdf_attachments';
const FEATURE_MORE_INVOICE_DESIGNS = 'more_invoice_designs'; const FEATURE_MORE_INVOICE_DESIGNS = 'more_invoice_designs';
const FEATURE_REPORTS = 'reports'; const FEATURE_REPORTS = 'reports';
const FEATURE_BUY_NOW_BUTTONS = 'buy_now_buttons'; const FEATURE_BUY_NOW_BUTTONS = 'buy_now_buttons';
const FEATURE_API = 'api'; const FEATURE_API = 'api';
const FEATURE_CLIENT_PORTAL_PASSWORD = 'client_portal_password'; const FEATURE_CLIENT_PORTAL_PASSWORD = 'client_portal_password';
const FEATURE_CUSTOM_URL = 'custom_url'; const FEATURE_CUSTOM_URL = 'custom_url';
const FEATURE_MORE_CLIENTS = 'more_clients'; const FEATURE_MORE_CLIENTS = 'more_clients';
const FEATURE_WHITE_LABEL = 'white_label'; const FEATURE_WHITE_LABEL = 'white_label';
const FEATURE_REMOVE_CREATED_BY = 'remove_created_by'; const FEATURE_REMOVE_CREATED_BY = 'remove_created_by';
const FEATURE_USERS = 'users'; // Grandfathered for old Pro users const FEATURE_USERS = 'users'; // Grandfathered for old Pro users
const FEATURE_DOCUMENTS = 'documents'; const FEATURE_DOCUMENTS = 'documents';
const FEATURE_USER_PERMISSIONS = 'permissions'; const FEATURE_USER_PERMISSIONS = 'permissions';
const RESULT_FAILURE = 'failure'; const RESULT_FAILURE = 'failure';
const RESULT_SUCCESS = 'success'; const RESULT_SUCCESS = 'success';
public function getEntityType() public function getEntityType()
@ -172,9 +150,8 @@ class Account extends BaseModel
public function getPlan() public function getPlan()
{ {
if (Carbon::parse($this->plan_expires)->lt(now())) { if(Carbon::parse($this->plan_expires)->lt(now()))
return ''; return '';
}
return $this->plan ?: ''; return $this->plan ?: '';
} }
@ -319,6 +296,7 @@ class Account extends BaseModel
if($trial_expires->greaterThan(now())){ if($trial_expires->greaterThan(now())){
$trial_active = true; $trial_active = true;
} }
} }
$plan_active = false; $plan_active = false;
@ -338,6 +316,7 @@ class Account extends BaseModel
return null; return null;
} }
// Should we show plan details or trial details? // Should we show plan details or trial details?
if (($plan && ! $trial_plan) || ! $include_trial) { if (($plan && ! $trial_plan) || ! $include_trial) {
$use_plan = true; $use_plan = true;
@ -394,14 +373,20 @@ class Account extends BaseModel
public function getDailyEmailLimit() public function getDailyEmailLimit()
{ {
if (Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0) { if($this->is_flagged)
return 0;
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0)
return 20;
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2 && !$this->payment_id)
return 20; return 20;
}
if($this->isPaid()){ if($this->isPaid()){
$limit = $this->paid_plan_email_quota; $limit = $this->paid_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100; $limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100;
} else { }
else{
$limit = $this->free_plan_email_quota; $limit = $this->free_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50; $limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50;
} }
@ -411,22 +396,22 @@ class Account extends BaseModel
public function emailsSent() public function emailsSent()
{ {
if (is_null(Cache::get($this->key))) { if(is_null(Cache::get($this->key)))
return 0; return 0;
}
return Cache::get($this->key); return Cache::get($this->key);
} }
public function emailQuotaExceeded() :bool public function emailQuotaExceeded() :bool
{ {
if (is_null(Cache::get($this->key))) { if(is_null(Cache::get($this->key)))
return false; return false;
}
try { try {
if(Cache::get($this->key) > $this->getDailyEmailLimit()) { if(Cache::get($this->key) > $this->getDailyEmailLimit()) {
if(is_null(Cache::get("throttle_notified:{$this->key}"))) { if(is_null(Cache::get("throttle_notified:{$this->key}"))) {
App::forgetInstance('translator'); App::forgetInstance('translator');
$t = app('translator'); $t = app('translator');
$t->replace(Ninja::transformTranslations($this->companies()->first()->settings)); $t->replace(Ninja::transformTranslations($this->companies()->first()->settings));
@ -440,14 +425,14 @@ class Account extends BaseModel
Cache::put("throttle_notified:{$this->key}", true, 60 * 24); Cache::put("throttle_notified:{$this->key}", true, 60 * 24);
if (config('ninja.notification.slack')) { if(config('ninja.notification.slack'))
$this->companies()->first()->notification(new EmailQuotaNotification($this))->ninja(); $this->companies()->first()->notification(new EmailQuotaNotification($this))->ninja();
} }
}
return true; return true;
} }
} catch (\Exception $e) { }
catch(\Exception $e){
\Sentry\captureMessage("I encountered an error with email quotas for account {$this->key} - defaulting to SEND"); \Sentry\captureMessage("I encountered an error with email quotas for account {$this->key} - defaulting to SEND");
} }
@ -456,16 +441,17 @@ class Account extends BaseModel
public function gmailCredentialNotification() :bool public function gmailCredentialNotification() :bool
{ {
nlog('checking if gmail credential notification has already been sent'); nlog("checking if gmail credential notification has already been sent");
if (is_null(Cache::get($this->key))) { if(is_null(Cache::get($this->key)))
return false; return false;
}
nlog('Sending notification'); nlog("Sending notification");
try { try {
if(is_null(Cache::get("gmail_credentials_notified:{$this->key}"))) { if(is_null(Cache::get("gmail_credentials_notified:{$this->key}"))) {
App::forgetInstance('translator'); App::forgetInstance('translator');
$t = app('translator'); $t = app('translator');
$t->replace(Ninja::transformTranslations($this->companies()->first()->settings)); $t->replace(Ninja::transformTranslations($this->companies()->first()->settings));
@ -479,17 +465,20 @@ class Account extends BaseModel
Cache::put("gmail_credentials_notified:{$this->key}", true, 60 * 24); Cache::put("gmail_credentials_notified:{$this->key}", true, 60 * 24);
if (config('ninja.notification.slack')) { if(config('ninja.notification.slack'))
$this->companies()->first()->notification(new GmailCredentialNotification($this))->ninja(); $this->companies()->first()->notification(new GmailCredentialNotification($this))->ninja();
} }
}
return true; return true;
} catch (\Exception $e) {
}
catch(\Exception $e){
\Sentry\captureMessage("I encountered an error with sending with gmail for account {$this->key}"); \Sentry\captureMessage("I encountered an error with sending with gmail for account {$this->key}");
} }
return false; return false;
} }
public function resolveRouteBinding($value, $field = null) public function resolveRouteBinding($value, $field = null)
@ -501,4 +490,18 @@ class Account extends BaseModel
return $this return $this
->where('id', $this->decodePrimaryKey($value))->firstOrFail(); ->where('id', $this->decodePrimaryKey($value))->firstOrFail();
} }
public function getTrialDays()
{
if($this->payment_id)
return 0;
$plan_expires = Carbon::parse($this->plan_expires);
if(!$this->payment_id && $plan_expires->gt(now()))
return $plan_expires->diffInDays();
return 0;
}
} }

View File

@ -203,7 +203,6 @@ class ClientContact extends Authenticatable implements HasLocalePreference
NinjaMailerJob::dispatch($nmo); NinjaMailerJob::dispatch($nmo);
//$this->notify(new ClientContactResetPassword($token));
} }
public function preferredLocale() public function preferredLocale()

View File

@ -56,6 +56,7 @@ class Expense extends BaseModel
'tax_amount3', 'tax_amount3',
'uses_inclusive_taxes', 'uses_inclusive_taxes',
'calculate_tax_by_amount', 'calculate_tax_by_amount',
'purchase_order_id',
]; ];
protected $casts = [ protected $casts = [
@ -102,6 +103,11 @@ class Expense extends BaseModel
return $this->belongsTo(Client::class); return $this->belongsTo(Client::class);
} }
public function purchase_order()
{
return $this->hasOne(PurchaseOrder::class);
}
public function translate_entity() public function translate_entity()
{ {
return ctrans('texts.expense'); return ctrans('texts.expense');

View File

@ -63,6 +63,8 @@ class Gateway extends StaticModel
$link = 'https://applications.sagepay.com/apply/2C02C252-0F8A-1B84-E10D-CF933EFCAA99'; $link = 'https://applications.sagepay.com/apply/2C02C252-0F8A-1B84-E10D-CF933EFCAA99';
} elseif ($this->id == 20 || $this->id == 56) { } elseif ($this->id == 20 || $this->id == 56) {
$link = 'https://dashboard.stripe.com/account/apikeys'; $link = 'https://dashboard.stripe.com/account/apikeys';
} elseif ($this->id == 59) {
$link = 'https://www.forte.net/';
} }
return $link; return $link;
@ -168,6 +170,12 @@ class Gateway extends StaticModel
GatewayType::HOSTED_PAGE => ['refund' => false, 'token_billing' => false, 'webhooks' => [' ']], // Razorpay GatewayType::HOSTED_PAGE => ['refund' => false, 'token_billing' => false, 'webhooks' => [' ']], // Razorpay
]; ];
break; break;
case 59:
return [
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], // Forte
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']],
];
break;
default: default:
return []; return [];
break; break;

View File

@ -18,78 +18,42 @@ class PaymentType extends StaticModel
*/ */
public $timestamps = false; public $timestamps = false;
const CREDIT = 1; const CREDIT = 32;
const ACH = 4; const ACH = 4;
const VISA = 5; const VISA = 5;
const MASTERCARD = 6; const MASTERCARD = 6;
const AMERICAN_EXPRESS = 7; const AMERICAN_EXPRESS = 7;
const DISCOVER = 8; const DISCOVER = 8;
const DINERS = 9; const DINERS = 9;
const EUROCARD = 10; const EUROCARD = 10;
const NOVA = 11; const NOVA = 11;
const CREDIT_CARD_OTHER = 12; const CREDIT_CARD_OTHER = 12;
const PAYPAL = 13; const PAYPAL = 13;
const CHECK = 15; const CHECK = 15;
const CARTE_BLANCHE = 16; const CARTE_BLANCHE = 16;
const UNIONPAY = 17; const UNIONPAY = 17;
const JCB = 18; const JCB = 18;
const LASER = 19; const LASER = 19;
const MAESTRO = 20; const MAESTRO = 20;
const SOLO = 21; const SOLO = 21;
const SWITCH = 22; const SWITCH = 22;
const ALIPAY = 27; const ALIPAY = 27;
const SOFORT = 28; const SOFORT = 28;
const SEPA = 29; const SEPA = 29;
const GOCARDLESS = 30; const GOCARDLESS = 30;
const CRYPTO = 31; const CRYPTO = 31;
const MOLLIE_BANK_TRANSFER = 34; const MOLLIE_BANK_TRANSFER = 34;
const KBC = 35; const KBC = 35;
const BANCONTACT = 36; const BANCONTACT = 36;
const IDEAL = 37; const IDEAL = 37;
const HOSTED_PAGE = 38; const HOSTED_PAGE = 38;
const GIROPAY = 39; const GIROPAY = 39;
const PRZELEWY24 = 40; const PRZELEWY24 = 40;
const EPS = 41; const EPS = 41;
const DIRECT_DEBIT = 42; const DIRECT_DEBIT = 42;
const BECS = 43; const BECS = 43;
const ACSS = 44; const ACSS = 44;
const INSTANT_BANK_PAY = 45; const INSTANT_BANK_PAY = 45;
const FPX = 46; const FPX = 46;
public static function parseCardType($cardName) public static function parseCardType($cardName)

View File

@ -125,6 +125,26 @@ class CompanyPresenter extends EntityPresenter
} }
} }
public function address1()
{
return $this->entity->settings->address1;
}
public function address2()
{
return $this->entity->settings->address2;
}
public function qr_iban()
{
return $this->entity->getSetting('qr_iban');
}
public function besr_id()
{
return $this->entity->getSetting('besr_id');
}
public function getSpcQrCode($client_currency, $invoice_number, $balance_due_raw, $user_iban) public function getSpcQrCode($client_currency, $invoice_number, $balance_due_raw, $user_iban)
{ {
$settings = $this->entity->settings; $settings = $this->entity->settings;

View File

@ -11,6 +11,7 @@
namespace App\Models; namespace App\Models;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive; use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Jobs\Entity\CreateEntityPdf; use App\Jobs\Entity\CreateEntityPdf;
@ -33,7 +34,6 @@ class PurchaseOrder extends BaseModel
'discount', 'discount',
'company_id', 'company_id',
'status_id', 'status_id',
'user_id',
'last_sent_date', 'last_sent_date',
'is_deleted', 'is_deleted',
'po_number', 'po_number',
@ -72,10 +72,6 @@ class PurchaseOrder extends BaseModel
'custom_surcharge2', 'custom_surcharge2',
'custom_surcharge3', 'custom_surcharge3',
'custom_surcharge4', 'custom_surcharge4',
// 'custom_surcharge_tax1',
// 'custom_surcharge_tax2',
// 'custom_surcharge_tax3',
// 'custom_surcharge_tax4',
'design_id', 'design_id',
'invoice_id', 'invoice_id',
'assigned_user_id', 'assigned_user_id',
@ -83,9 +79,8 @@ class PurchaseOrder extends BaseModel
'balance', 'balance',
'partial', 'partial',
'paid_to_date', 'paid_to_date',
// 'subscription_id',
'vendor_id', 'vendor_id',
'last_viewed', 'last_viewed'
]; ];
protected $casts = [ protected $casts = [
@ -99,12 +94,10 @@ class PurchaseOrder extends BaseModel
]; ];
const STATUS_DRAFT = 1; const STATUS_DRAFT = 1;
const STATUS_SENT = 2; const STATUS_SENT = 2;
const STATUS_ACCEPTED = 3; const STATUS_ACCEPTED = 3;
const STATUS_RECEIVED = 4;
const STATUS_CANCELLED = 4; const STATUS_CANCELLED = 5;
public static function stringStatus(int $status) public static function stringStatus(int $status)
{ {
@ -126,6 +119,7 @@ class PurchaseOrder extends BaseModel
} }
} }
public static function badgeForStatus(int $status) public static function badgeForStatus(int $status)
{ {
switch ($status) { switch ($status) {
@ -147,6 +141,7 @@ class PurchaseOrder extends BaseModel
} }
} }
public function assigned_user() public function assigned_user()
{ {
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed(); return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
@ -154,7 +149,7 @@ class PurchaseOrder extends BaseModel
public function vendor() public function vendor()
{ {
return $this->belongsTo(Vendor::class); return $this->belongsTo(Vendor::class)->withTrashed();
} }
public function history() public function history()
@ -172,6 +167,11 @@ class PurchaseOrder extends BaseModel
return $this->belongsTo(Company::class); return $this->belongsTo(Company::class);
} }
public function expense()
{
return $this->belongsTo(Expense::class);
}
public function user() public function user()
{ {
return $this->belongsTo(User::class)->withTrashed(); return $this->belongsTo(User::class)->withTrashed();
@ -181,7 +181,6 @@ class PurchaseOrder extends BaseModel
{ {
return $this->belongsTo(Client::class)->withTrashed(); return $this->belongsTo(Client::class)->withTrashed();
} }
public function markInvitationsSent() public function markInvitationsSent()
{ {
$this->invitations->each(function ($invitation) { $this->invitations->each(function ($invitation) {
@ -195,33 +194,33 @@ class PurchaseOrder extends BaseModel
public function pdf_file_path($invitation = null, string $type = 'path', bool $portal = false) public function pdf_file_path($invitation = null, string $type = 'path', bool $portal = false)
{ {
if (! $invitation) { if (! $invitation) {
if ($this->invitations()->exists()) {
if($this->invitations()->exists())
$invitation = $this->invitations()->first(); $invitation = $this->invitations()->first();
} else { else{
$this->service()->createInvitations(); $this->service()->createInvitations();
$invitation = $this->invitations()->first(); $invitation = $this->invitations()->first();
} }
} }
if (! $invitation) { if(!$invitation)
throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?'); throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?');
}
$file_path = $this->vendor->purchase_order_filepath($invitation).$this->numberFormatter().'.pdf'; $file_path = $this->vendor->purchase_order_filepath($invitation).$this->numberFormatter().'.pdf';
if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){ if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){
return Storage::disk(config('filesystems.default'))->{$type}($file_path); return Storage::disk(config('filesystems.default'))->{$type}($file_path);
} elseif (Ninja::isHosted() && $portal) { }
$file_path = (new CreatePurchaseOrderPdf($invitation, config('filesystems.default')))->handle(); elseif(Ninja::isHosted() && $portal){
$file_path = CreatePurchaseOrderPdf::dispatchNow($invitation,config('filesystems.default'));
return Storage::disk(config('filesystems.default'))->{$type}($file_path); return Storage::disk(config('filesystems.default'))->{$type}($file_path);
} }
if (Storage::disk('public')->exists($file_path)) { if(Storage::disk('public')->exists($file_path))
return Storage::disk('public')->{$type}($file_path); return Storage::disk('public')->{$type}($file_path);
}
$file_path = (new CreatePurchaseOrderPdf($invitation))->handle(); $file_path = CreatePurchaseOrderPdf::dispatchNow($invitation);
return Storage::disk('public')->{$type}($file_path); return Storage::disk('public')->{$type}($file_path);
} }
@ -272,4 +271,5 @@ class PurchaseOrder extends BaseModel
return $purchase_order_calc->build(); return $purchase_order_calc->build();
} }
} }

View File

@ -104,6 +104,7 @@ class User extends Authenticatable implements MustVerifyEmail
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
'oauth_user_token_expiry' => 'datetime',
]; ];
public function name() public function name()

View File

@ -0,0 +1,93 @@
<?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\Notifications\Ninja;
use App\Models\Company;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class EmailQualityNotification extends Notification
{
/**
* Create a new notification instance.
*
* @return void
*/
protected Company $company;
protected string $spam_string;
public function __construct(Company $company, string $spam_string)
{
$this->company = $company;
$this->spam_string = $spam_string;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return MailMessage
*/
public function toMail($notifiable)
{
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$content = "Email Quality notification for Company {$this->company->company_key} \n";
$owner = $this->company->owner();
$content .= "Owner {$owner->present()->name() } | {$owner->email} \n";
$content .= "Spam trigger: {$this->spam_string}";
return (new SlackMessage)
->success()
->from(ctrans('texts.notification_bot'))
->image('https://app.invoiceninja.com/favicon.png')
->content($content);
}
}

View File

@ -0,0 +1,93 @@
<?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\Notifications\Ninja;
use App\Models\Account;
use App\Models\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class NewAccountNotification extends Notification
{
/**
* Create a new notification instance.
*
* @return void
*/
protected Account $account;
protected Client $client;
public function __construct(Account $account, Client $client)
{
$this->account = $account;
$this->client = $client;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return MailMessage
*/
public function toMail($notifiable)
{
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$content = "New Trial Started\n";
$content .= "{$this->client->name}\n";
$content .= "Account key: {$this->account->key}\n";
$content .= "Users: {$this->account->users()->pluck('email')}\n";
$content .= "Contacts: {$this->client->contacts()->pluck('email')}\n";
return (new SlackMessage)
->success()
->from(ctrans('texts.notification_bot'))
->image('https://app.invoiceninja.com/favicon.png')
->content($content);
}
}

View File

@ -0,0 +1,121 @@
<?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\Notifications\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SpamNotification extends Notification
{
/**
* Create a new notification instance.
*
* @return void
*/
protected array $spam_list;
public function __construct($spam_list)
{
$this->spam_list = $spam_list;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return MailMessage
*/
public function toMail($notifiable)
{
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$content = '';
foreach($this->spam_list as $spam_list)
{
if(array_key_exists('companies', $spam_list))
{
$content .= " Companies \n";
foreach($spam_list['companies'] as $company)
{
$content .= "{$company['name']} - c_key={$company['company_key']} - a_key={$company['account_key']} - {$company['owner']} \n";
}
}
if(array_key_exists('templates', $spam_list))
{
$content .= " Templates \n";
foreach($spam_list['templates'] as $company)
{
$content .= "{$company['name']} - c_key={$company['company_key']} - a_key={$company['account_key']} - {$company['owner']} \n";
}
}
if(array_key_exists('users', $spam_list))
{
$content .= ' Users \n';
foreach($spam_list['users'] as $user)
{
$content .= "{$user['email']} - a_key={$user['account_key']} - created={$user['created']} \n";
}
}
}
return (new SlackMessage)
->success()
->from(ctrans('texts.notification_bot'))
->image('https://app.invoiceninja.com/favicon.png')
->content($content);
}
}

View File

@ -66,7 +66,19 @@ class AuthorizePaymentDriver extends BaseDriver
public function getClientRequiredFields(): array public function getClientRequiredFields(): array
{ {
return [
$fields = [];
if ($this->company_gateway->require_shipping_address) {
$fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_shipping_city', 'label' => ctrans('texts.shipping_city'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_shipping_state', 'label' => ctrans('texts.shipping_state'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_shipping_postal_code', 'label' => ctrans('texts.shipping_postal_code'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_shipping_country_id', 'label' => ctrans('texts.shipping_country'), 'type' => 'text', 'validation' => 'required'];
}
$data = [
['name' => 'client_name', 'label' => ctrans('texts.name'), 'type' => 'text', 'validation' => 'required|min:2'], ['name' => 'client_name', 'label' => ctrans('texts.name'), 'type' => 'text', 'validation' => 'required|min:2'],
['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required|email:rfc'], ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required|email:rfc'],
['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'], ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'],
@ -75,6 +87,9 @@ class AuthorizePaymentDriver extends BaseDriver
['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'], ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'],
['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'select', 'validation' => 'required'], ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'select', 'validation' => 'required'],
]; ];
return array_merge($fields, $data);
} }
public function authorizeView($payment_method) public function authorizeView($payment_method)

View File

@ -0,0 +1,150 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Forte;
use App\Models\Payment;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Validator;
use App\PaymentDrivers\FortePaymentDriver;
class ACH
{
use MakesHash;
public $forte;
private $forte_base_uri="";
private $forte_api_access_id="";
private $forte_secure_key="";
private $forte_auth_organization_id="";
private $forte_organization_id="";
private $forte_location_id="";
public function __construct(FortePaymentDriver $forte)
{
$this->forte = $forte;
$this->forte_base_uri = "https://sandbox.forte.net/api/v3/";
if($this->forte->company_gateway->getConfigField('testMode') == false){
$this->forte_base_uri = "https://api.forte.net/v3/";
}
$this->forte_api_access_id = $this->forte->company_gateway->getConfigField('apiAccessId');
$this->forte_secure_key = $this->forte->company_gateway->getConfigField('secureKey');
$this->forte_auth_organization_id = $this->forte->company_gateway->getConfigField('authOrganizationId');
$this->forte_organization_id = $this->forte->company_gateway->getConfigField('organizationId');
$this->forte_location_id = $this->forte->company_gateway->getConfigField('locationId');
}
public function authorizeView(array $data)
{
return render('gateways.forte.ach.authorize', $data);
}
public function authorizeResponse(Request $request)
{
$payment_meta = new \stdClass;
$payment_meta->brand = (string)ctrans('texts.ach');
$payment_meta->last4 = (string) $request->last_4;
$payment_meta->exp_year = '-';
$payment_meta->type = GatewayType::BANK_TRANSFER;
$data = [
'payment_meta' => $payment_meta,
'token' => $request->one_time_token,
'payment_method_id' => $request->gateway_type_id,
];
$this->forte->storeGatewayToken($data);
return redirect()->route('client.payment_methods.index')->withSuccess('Payment Method added.');
}
public function paymentView(array $data)
{
$this->forte->payment_hash->data = array_merge((array) $this->forte->payment_hash->data, $data);
$this->forte->payment_hash->save();
$data['gateway'] = $this;
return render('gateways.forte.ach.pay', $data);
}
public function paymentResponse($request)
{
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->firstOrFail();
try {
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $this->forte_base_uri.'organizations/'.$this->forte_organization_id.'/locations/'.$this->forte_location_id.'/transactions',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS =>'{
"action":"sale",
"authorization_amount": '.$payment_hash->data->total->amount_with_fee.',
"echeck":{
"sec_code":"PPD",
},
"billing_address":{
"first_name": "'.$this->forte->client->name.'",
"last_name": "'.$this->forte->client->name.'"
},
"echeck":{
"one_time_token":"'.$request->payment_token.'"
}
}',
CURLOPT_HTTPHEADER => array(
'X-Forte-Auth-Organization-Id: '.$this->forte_organization_id,
'Content-Type: application/json',
'Authorization: Basic '.base64_encode($this->forte_api_access_id.':'.$this->forte_secure_key),
'Cookie: visid_incap_621087=u18+3REYR/iISgzZxOF5s2ODW2IAAAAAQUIPAAAAAADuGqKgECQLS81FcSDrmhGe; nlbi_621087=YHngadhJ2VU+yr7/R1efXgAAAAD3mQyhqmnLls8PRu4iN58G; incap_ses_1136_621087=CVdrXUdhIlm9WJNDieLDD4QVXGIAAAAAvBwvkUcwhM0+OwvdPm2stg=='
),
));
$response = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
$response=json_decode($response);
} catch (\Throwable $th) {
throw $th;
}
if ($httpcode>299) {
$error = Validator::make([], []);
$error->getMessageBag()->add('gateway_error', $response->response->response_desc);
return redirect('client/invoices')->withErrors($error);
}
$data = [
'payment_method' => $request->payment_method_id,
'payment_type' => PaymentType::ACH,
'amount' => $payment_hash->data->amount_with_fee,
'transaction_reference' => $response->transaction_id,
'gateway_type_id' => GatewayType::BANK_TRANSFER,
];
$payment=$this->forte->createPayment($data, Payment::STATUS_COMPLETED);
return redirect('client/invoices')->withSuccess('Invoice paid.');
}
}

View File

@ -0,0 +1,160 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Forte;
use App\Models\Payment;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use App\PaymentDrivers\FortePaymentDriver;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
class CreditCard
{
use MakesHash;
public $forte;
private $forte_base_uri="";
private $forte_api_access_id="";
private $forte_secure_key="";
private $forte_auth_organization_id="";
private $forte_organization_id="";
private $forte_location_id="";
public function __construct(FortePaymentDriver $forte)
{
$this->forte = $forte;
$this->forte_base_uri = "https://sandbox.forte.net/api/v3/";
if($this->forte->company_gateway->getConfigField('testMode') == false){
$this->forte_base_uri = "https://api.forte.net/v3/";
}
$this->forte_api_access_id = $this->forte->company_gateway->getConfigField('apiAccessId');
$this->forte_secure_key = $this->forte->company_gateway->getConfigField('secureKey');
$this->forte_auth_organization_id = $this->forte->company_gateway->getConfigField('authOrganizationId');
$this->forte_organization_id = $this->forte->company_gateway->getConfigField('organizationId');
$this->forte_location_id = $this->forte->company_gateway->getConfigField('locationId');
}
public function authorizeView(array $data)
{
return render('gateways.forte.credit_card.authorize', $data);
}
public function authorizeResponse($request)
{
$payment_meta = new \stdClass;
$payment_meta->exp_month = (string) $request->expire_month;
$payment_meta->exp_year = (string) $request->expire_year;
$payment_meta->brand = (string) $request->card_type;
$payment_meta->last4 = (string) $request->last_4;
$payment_meta->type = GatewayType::CREDIT_CARD;
$data = [
'payment_meta' => $payment_meta,
'token' => $request->one_time_token,
'payment_method_id' => $request->payment_method_id,
];
$this->forte->storeGatewayToken($data);
return redirect()->route('client.payment_methods.index')->withSuccess('Payment Method added.');
}
public function paymentView(array $data)
{
$this->forte->payment_hash->data = array_merge((array) $this->forte->payment_hash->data, $data);
$this->forte->payment_hash->save();
$data['gateway'] = $this;
return render('gateways.forte.credit_card.pay', $data);
}
public function paymentResponse(PaymentResponseRequest $request)
{
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->firstOrFail();
$amount_with_fee = $payment_hash->data->total->amount_with_fee;
$invoice_totals = $payment_hash->data->total->invoice_totals;
$fee_total = 0;
for ($i = ($invoice_totals * 100) ; $i < ($amount_with_fee * 100); $i++) {
$calculated_fee = ( 3 * $i) / 100;
$calculated_amount_with_fee = round(($i + $calculated_fee) / 100,2);
if ($calculated_amount_with_fee == $amount_with_fee) {
$fee_total = round($calculated_fee / 100,2);
$amount_with_fee = $calculated_amount_with_fee;
break;
}
}
try {
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $this->forte_base_uri.'organizations/'.$this->forte_organization_id.'/locations/'.$this->forte_location_id.'/transactions',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS =>'{
"action":"sale",
"authorization_amount":'.$amount_with_fee.',
"service_fee_amount":'.$fee_total.',
"billing_address":{
"first_name":"'.$this->forte->client->name.'",
"last_name":"'.$this->forte->client->name.'"
},
"card":{
"one_time_token":"'.$request->payment_token.'"
}
}',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json',
'X-Forte-Auth-Organization-Id: '.$this->forte_organization_id,
'Authorization: Basic '.base64_encode($this->forte_api_access_id.':'.$this->forte_secure_key)
),
));
$response = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
$response=json_decode($response);
} catch (\Throwable $th) {
throw $th;
}
if ($httpcode>299) {
$error = Validator::make([], []);
$error->getMessageBag()->add('gateway_error', $response->response->response_desc);
return redirect('client/invoices')->withErrors($error);
}
$data = [
'payment_method' => $request->payment_method_id,
'payment_type' => PaymentType::parseCardType(strtolower($request->card_brand)) ?: PaymentType::CREDIT_CARD_OTHER,
'amount' => $payment_hash->data->amount_with_fee,
'transaction_reference' => $response->transaction_id,
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
$payment=$this->forte->createPayment($data, Payment::STATUS_COMPLETED);
return redirect('client/invoices')->withSuccess('Invoice paid.');
}
}

View File

@ -0,0 +1,90 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Utils\Traits\MakesHash;
use App\PaymentDrivers\Forte\ACH;
use App\PaymentDrivers\Forte\CreditCard;
class FortePaymentDriver extends BaseDriver
{
use MakesHash;
public $refundable = true; //does this gateway support refunds?
public $token_billing = true; //does this gateway support token billing?
public $can_authorise_credit_card = true; //does this gateway support authorizations?
public $gateway; //initialized gateway
public $payment_method; //initialized payment method
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
GatewayType::BANK_TRANSFER => ACH::class,
];
/**
* Returns the gateway types.
*/
public function gatewayTypes(): array
{
$types = [];
$types[] = GatewayType::CREDIT_CARD;
$types[] = GatewayType::BANK_TRANSFER;
return $types;
}
const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
public function authorizeView(array $data)
{
return $this->payment_method->authorizeView($data); //this is your custom implementation from here
}
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request); //this is your custom implementation from here
}
public function processPaymentView(array $data)
{
return $this->payment_method->paymentView($data); //this is your custom implementation from here
}
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request); //this is your custom implementation from here
}
// public function refund(Payment $payment, $amount, $return_client_response = false)
// {
// return $this->payment_method->yourRefundImplementationHere(); //this is your custom implementation from here
// }
// public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
// {
// return $this->payment_method->yourTokenBillingImplmentation(); //this is your custom implementation from here
// }
}

View File

@ -53,12 +53,15 @@ class PaymentIntentWebhook implements ShouldQueue
public function handle() public function handle()
{ {
MultiDB::findAndSetDbByCompanyKey($this->company_key); MultiDB::findAndSetDbByCompanyKey($this->company_key);
$company = Company::where('company_key', $this->company_key)->first(); $company = Company::where('company_key', $this->company_key)->first();
foreach ($this->stripe_request as $transaction) { foreach ($this->stripe_request as $transaction) {
if (array_key_exists('payment_intent', $transaction)) {
if(array_key_exists('payment_intent', $transaction))
{
$payment = Payment::query() $payment = Payment::query()
->where('company_id', $company->id) ->where('company_id', $company->id)
->where(function ($query) use ($transaction) { ->where(function ($query) use ($transaction) {
@ -66,7 +69,9 @@ class PaymentIntentWebhook implements ShouldQueue
->orWhere('transaction_reference', $transaction['id']); ->orWhere('transaction_reference', $transaction['id']);
}) })
->first(); ->first();
} else { }
else
{
$payment = Payment::query() $payment = Payment::query()
->where('company_id', $company->id) ->where('company_id', $company->id)
->where('transaction_reference', $transaction['id']) ->where('transaction_reference', $transaction['id'])
@ -81,11 +86,13 @@ class PaymentIntentWebhook implements ShouldQueue
} }
} }
if ($this->payment_completed) {
if($this->payment_completed)
return; return;
}
if(optional($this->stripe_request['object']['charges']['data'][0])['id']){ if(optional($this->stripe_request['object']['charges']['data'][0])['id']){
$company = Company::where('company_key', $this->company_key)->first(); $company = Company::where('company_key', $this->company_key)->first();
$payment = Payment::query() $payment = Payment::query()
@ -95,27 +102,28 @@ class PaymentIntentWebhook implements ShouldQueue
//return early //return early
if($payment && $payment->status_id == Payment::STATUS_COMPLETED){ if($payment && $payment->status_id == Payment::STATUS_COMPLETED){
nlog(' payment found and status correct - returning '); nlog(" payment found and status correct - returning ");
return; return;
} elseif ($payment) { }
elseif($payment){
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;
$payment->save(); $payment->save();
} }
$hash = optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['payment_hash']; $hash = optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['payment_hash'];
$payment_hash = PaymentHash::where('hash', $hash)->first(); $payment_hash = PaymentHash::where('hash', $hash)->first();
if (! $payment_hash) { if(!$payment_hash)
return; return;
}
nlog('payment intent'); nlog("payment intent");
nlog($this->stripe_request); nlog($this->stripe_request);
if (optional($this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash']) && in_array('card', $this->stripe_request['object']['allowed_source_types'])) { if(array_key_exists('allowed_source_types', $this->stripe_request['object']) && optional($this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash']) && in_array('card', $this->stripe_request['object']['allowed_source_types']))
nlog('hash found'); {
nlog("hash found");
$hash = $this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash']; $hash = $this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash'];
@ -125,7 +133,21 @@ class PaymentIntentWebhook implements ShouldQueue
$this->updateCreditCardPayment($payment_hash, $client); $this->updateCreditCardPayment($payment_hash, $client);
} }
elseif(array_key_exists('payment_method_types', $this->stripe_request['object']) && optional($this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash']) && in_array('card', $this->stripe_request['object']['payment_method_types']))
{
nlog("hash found");
$hash = $this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash'];
$payment_hash = PaymentHash::where('hash', $hash)->first();
$invoice = Invoice::with('client')->find($payment_hash->fee_invoice_id);
$client = $invoice->client;
$this->updateCreditCardPayment($payment_hash, $client);
} }
}
} }
private function updateCreditCardPayment($payment_hash, $client) private function updateCreditCardPayment($payment_hash, $client)
@ -156,5 +178,7 @@ class PaymentIntentWebhook implements ShouldQueue
$client, $client,
$client->company, $client->company,
); );
} }
} }

View File

@ -32,7 +32,6 @@ class BaseRepository
public bool $import_mode = false; public bool $import_mode = false;
private bool $new_model = false; private bool $new_model = false;
/** /**
* @param $entity * @param $entity
* @param $type * @param $type
@ -138,9 +137,8 @@ class BaseRepository
/* Returns an invoice if defined as a key in the $resource array*/ /* Returns an invoice if defined as a key in the $resource array*/
public function getInvitation($invitation, $resource) public function getInvitation($invitation, $resource)
{ {
if (is_array($invitation) && ! array_key_exists('key', $invitation)) { if (is_array($invitation) && ! array_key_exists('key', $invitation))
return false; return false;
}
$invitation_class = sprintf('App\\Models\\%sInvitation', $resource); $invitation_class = sprintf('App\\Models\\%sInvitation', $resource);
@ -153,13 +151,13 @@ class BaseRepository
private function resolveEntityKey($model) private function resolveEntityKey($model)
{ {
switch ($model) { switch ($model) {
case $model instanceof RecurringInvoice: case ($model instanceof RecurringInvoice):
return 'recurring_invoice_id'; return 'recurring_invoice_id';
case $model instanceof Invoice: case ($model instanceof Invoice):
return 'invoice_id'; return 'invoice_id';
case $model instanceof Quote: case ($model instanceof Quote):
return 'quote_id'; return 'quote_id';
case $model instanceof Credit: case ($model instanceof Credit):
return 'credit_id'; return 'credit_id';
} }
} }
@ -175,9 +173,8 @@ class BaseRepository
protected function alternativeSave($data, $model) protected function alternativeSave($data, $model)
{ {
//forces the client_id if it doesn't exist //forces the client_id if it doesn't exist
if (array_key_exists('client_id', $data)) { if(array_key_exists('client_id', $data))
$model->client_id = $data['client_id']; $model->client_id = $data['client_id'];
}
$client = Client::where('id', $model->client_id)->withTrashed()->firstOrFail(); $client = Client::where('id', $model->client_id)->withTrashed()->firstOrFail();
@ -198,13 +195,11 @@ class BaseRepository
$tmp_data = $data; //preserves the $data array $tmp_data = $data; //preserves the $data array
/* We need to unset some variable as we sometimes unguard the model */ /* We need to unset some variable as we sometimes unguard the model */
if (isset($tmp_data['invitations'])) { if (isset($tmp_data['invitations']))
unset($tmp_data['invitations']); unset($tmp_data['invitations']);
}
if (isset($tmp_data['client_contacts'])) { if (isset($tmp_data['client_contacts']))
unset($tmp_data['client_contacts']); unset($tmp_data['client_contacts']);
}
$model->fill($tmp_data); $model->fill($tmp_data);
@ -213,26 +208,22 @@ class BaseRepository
$model->custom_surcharge_tax3 = $client->company->custom_surcharge_taxes3; $model->custom_surcharge_tax3 = $client->company->custom_surcharge_taxes3;
$model->custom_surcharge_tax4 = $client->company->custom_surcharge_taxes4; $model->custom_surcharge_tax4 = $client->company->custom_surcharge_taxes4;
if (! $model->id) { if(!$model->id)
$this->new_model = true; $this->new_model = true;
}
$model->saveQuietly(); $model->saveQuietly();
/* Model now persisted, now lets do some child tasks */ /* Model now persisted, now lets do some child tasks */
if ($model instanceof Invoice) { if($model instanceof Invoice)
$model->service()->setReminder()->save(); $model->service()->setReminder()->save();
}
/* Save any documents */ /* Save any documents */
if (array_key_exists('documents', $data)) { if (array_key_exists('documents', $data))
$this->saveDocuments($data['documents'], $model); $this->saveDocuments($data['documents'], $model);
}
if (array_key_exists('file', $data)) { if (array_key_exists('file', $data))
$this->saveDocuments($data['file'], $model); $this->saveDocuments($data['file'], $model);
}
/* If invitations are present we need to filter existing invitations with the new ones */ /* If invitations are present we need to filter existing invitations with the new ones */
if (isset($data['invitations'])) { if (isset($data['invitations'])) {
@ -243,23 +234,24 @@ class BaseRepository
$invitation_class = sprintf('App\\Models\\%sInvitation', $resource); $invitation_class = sprintf('App\\Models\\%sInvitation', $resource);
$invitation = $invitation_class::where('key', $invitation)->first(); $invitation = $invitation_class::where('key', $invitation)->first();
if ($invitation) { if ($invitation)
$invitation->delete(); $invitation->delete();
}
}); });
foreach ($data['invitations'] as $invitation) { foreach ($data['invitations'] as $invitation) {
//if no invitations are present - create one. //if no invitations are present - create one.
if (! $this->getInvitation($invitation, $resource)) { if (! $this->getInvitation($invitation, $resource)) {
if (isset($invitation['id'])) {
if (isset($invitation['id']))
unset($invitation['id']); unset($invitation['id']);
}
//make sure we are creating an invite for a contact who belongs to the client only! //make sure we are creating an invite for a contact who belongs to the client only!
$contact = ClientContact::find($invitation['client_contact_id']); $contact = ClientContact::find($invitation['client_contact_id']);
if ($contact && $model->client_id == $contact->client_id) { if ($contact && $model->client_id == $contact->client_id) {
$invitation_class = sprintf('App\\Models\\%sInvitation', $resource); $invitation_class = sprintf('App\\Models\\%sInvitation', $resource);
$new_invitation = $invitation_class::withTrashed() $new_invitation = $invitation_class::withTrashed()
@ -268,14 +260,18 @@ class BaseRepository
->first(); ->first();
if ($new_invitation && $new_invitation->trashed()) { if ($new_invitation && $new_invitation->trashed()) {
$new_invitation->restore(); $new_invitation->restore();
} else { } else {
$invitation_factory_class = sprintf('App\\Factory\\%sInvitationFactory', $resource); $invitation_factory_class = sprintf('App\\Factory\\%sInvitationFactory', $resource);
$new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id); $new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id);
$new_invitation->{$lcfirst_resource_id} = $model->id; $new_invitation->{$lcfirst_resource_id} = $model->id;
$new_invitation->client_contact_id = $contact->id; $new_invitation->client_contact_id = $contact->id;
$new_invitation->key = $this->createDbHash($model->company->db); $new_invitation->key = $this->createDbHash($model->company->db);
$new_invitation->save(); $new_invitation->save();
} }
} }
} }
@ -283,9 +279,8 @@ class BaseRepository
} }
/* If no invitations have been created, this is our fail safe to maintain state*/ /* If no invitations have been created, this is our fail safe to maintain state*/
if ($model->invitations()->count() == 0) { if ($model->invitations()->count() == 0)
$model->service()->createInvitations(); $model->service()->createInvitations();
}
/* Recalculate invoice amounts */ /* Recalculate invoice amounts */
$model = $model->calc()->getInvoice(); $model = $model->calc()->getInvoice();
@ -297,82 +292,83 @@ class BaseRepository
$model = $model->service()->applyNumber()->save(); $model = $model->service()->applyNumber()->save();
/* Handle attempts where the deposit is greater than the amount/balance of the invoice */ /* Handle attempts where the deposit is greater than the amount/balance of the invoice */
if ((int) $model->balance != 0 && $model->partial > $model->amount) { if((int)$model->balance != 0 && $model->partial > $model->amount)
$model->partial = min($model->amount, $model->balance); $model->partial = min($model->amount, $model->balance);
}
/* Update product details if necessary - if we are inside a transaction - do nothing */ /* Update product details if necessary - if we are inside a transaction - do nothing */
if ($model->company->update_products && $model->id && \DB::transactionLevel() == 0) { if ($model->company->update_products && $model->id && \DB::transactionLevel() == 0)
UpdateOrCreateProduct::dispatch($model->line_items, $model, $model->company); UpdateOrCreateProduct::dispatch($model->line_items, $model, $model->company);
}
/* Perform model specific tasks */ /* Perform model specific tasks */
if ($model instanceof Invoice) { if ($model instanceof Invoice) {
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) { if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {
//10-07-2022
$model->service()->updateStatus()->save(); $model->service()->updateStatus()->save();
$model->ledger()->updateInvoiceBalance(($state['finished_amount'] - $state['starting_amount']), "Update adjustment for invoice {$model->number}");
$model->client->service()->updateBalance(($state['finished_amount'] - $state['starting_amount']))->save(); $model->client->service()->updateBalance(($state['finished_amount'] - $state['starting_amount']))->save();
$model->ledger()->updateInvoiceBalance(($state['finished_amount'] - $state['starting_amount']), "Update adjustment for invoice {$model->number}");
} }
if (! $model->design_id) { if (! $model->design_id)
$model->design_id = $this->decodePrimaryKey($client->getSetting('invoice_design_id')); $model->design_id = $this->decodePrimaryKey($client->getSetting('invoice_design_id'));
}
//links tasks and expenses back to the invoice. //links tasks and expenses back to the invoice.
$model->service()->linkEntities()->save(); $model->service()->linkEntities()->save();
if ($this->new_model) { if($this->new_model)
event('eloquent.created: App\Models\Invoice', $model); event('eloquent.created: App\Models\Invoice', $model);
} else { else
event('eloquent.updated: App\Models\Invoice', $model); event('eloquent.updated: App\Models\Invoice', $model);
}
} }
if ($model instanceof Credit) { if ($model instanceof Credit) {
$model = $model->calc()->getCredit(); $model = $model->calc()->getCredit();
if (! $model->design_id) { if (! $model->design_id)
$model->design_id = $this->decodePrimaryKey($client->getSetting('credit_design_id')); $model->design_id = $this->decodePrimaryKey($client->getSetting('credit_design_id'));
}
if (array_key_exists('invoice_id', $data) && $data['invoice_id']) { if(array_key_exists('invoice_id', $data) && $data['invoice_id'])
$model->invoice_id = $data['invoice_id']; $model->invoice_id = $data['invoice_id'];
}
if ($this->new_model) { if($this->new_model)
event('eloquent.created: App\Models\Credit', $model); event('eloquent.created: App\Models\Credit', $model);
} else { else
event('eloquent.updated: App\Models\Credit', $model); event('eloquent.updated: App\Models\Credit', $model);
}
} }
if ($model instanceof Quote) { if ($model instanceof Quote) {
if (! $model->design_id) {
if (! $model->design_id)
$model->design_id = $this->decodePrimaryKey($client->getSetting('quote_design_id')); $model->design_id = $this->decodePrimaryKey($client->getSetting('quote_design_id'));
}
$model = $model->calc()->getQuote(); $model = $model->calc()->getQuote();
if ($this->new_model) {
if($this->new_model)
event('eloquent.created: App\Models\Quote', $model); event('eloquent.created: App\Models\Quote', $model);
} else { else
event('eloquent.updated: App\Models\Quote', $model); event('eloquent.updated: App\Models\Quote', $model);
} }
}
if ($model instanceof RecurringInvoice) { if ($model instanceof RecurringInvoice) {
if (! $model->design_id) {
if (! $model->design_id)
$model->design_id = $this->decodePrimaryKey($client->getSetting('invoice_design_id')); $model->design_id = $this->decodePrimaryKey($client->getSetting('invoice_design_id'));
}
$model = $model->calc()->getRecurringInvoice(); $model = $model->calc()->getRecurringInvoice();
if ($this->new_model) {
if($this->new_model)
event('eloquent.created: App\Models\RecurringInvoice', $model); event('eloquent.created: App\Models\RecurringInvoice', $model);
} else { else
event('eloquent.updated: App\Models\RecurringInvoice', $model); event('eloquent.updated: App\Models\RecurringInvoice', $model);
} }
}
$model->save(); $model->save();

View File

@ -30,15 +30,13 @@ use Illuminate\Support\Carbon;
/** /**
* PaymentRepository. * PaymentRepository.
*/ */
class PaymentRepository extends BaseRepository class PaymentRepository extends BaseRepository {
{
use MakesHash; use MakesHash;
use SavesDocuments; use SavesDocuments;
protected $credit_repo; protected $credit_repo;
public function __construct(CreditRepository $credit_repo) public function __construct( CreditRepository $credit_repo ) {
{
$this->credit_repo = $credit_repo; $this->credit_repo = $credit_repo;
} }
@ -62,6 +60,7 @@ class PaymentRepository extends BaseRepository
*/ */
private function applyPayment(array $data, Payment $payment): ?Payment private function applyPayment(array $data, Payment $payment): ?Payment
{ {
$is_existing_payment = true; $is_existing_payment = true;
$client = false; $client = false;
@ -70,9 +69,8 @@ class PaymentRepository extends BaseRepository
$payment = $this->processExchangeRates($data, $payment); $payment = $this->processExchangeRates($data, $payment);
/* This is needed here otherwise the ->fill() overwrites anything that exists*/ /* This is needed here otherwise the ->fill() overwrites anything that exists*/
if ($payment->exchange_rate != 1) { if($payment->exchange_rate != 1)
unset($data['exchange_rate']); unset($data['exchange_rate']);
}
$is_existing_payment = false; $is_existing_payment = false;
$client = Client::where('id', $data['client_id'])->withTrashed()->first(); $client = Client::where('id', $data['client_id'])->withTrashed()->first();
@ -85,7 +83,7 @@ class PaymentRepository extends BaseRepository
$client->service()->updatePaidToDate($data['amount'])->save(); $client->service()->updatePaidToDate($data['amount'])->save();
} }
// elseif($data['amount'] >0){
else{ else{
//this fixes an edge case with unapplied payments //this fixes an edge case with unapplied payments
$client->service()->updatePaidToDate($data['amount'])->save(); $client->service()->updatePaidToDate($data['amount'])->save();
@ -95,7 +93,9 @@ class PaymentRepository extends BaseRepository
$_credit_totals = array_sum(array_column($data['credits'], 'amount')); $_credit_totals = array_sum(array_column($data['credits'], 'amount'));
$client->service()->updatePaidToDate($_credit_totals)->save(); $client->service()->updatePaidToDate($_credit_totals)->save();
} }
} }
/*Fill the payment*/ /*Fill the payment*/
@ -104,11 +104,12 @@ class PaymentRepository extends BaseRepository
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;
if (! $payment->currency_id && $client) { if (! $payment->currency_id && $client) {
if (property_exists($client->settings, 'currency_id')) {
if(property_exists($client->settings, 'currency_id'))
$payment->currency_id = $client->settings->currency_id; $payment->currency_id = $client->settings->currency_id;
} else { else
$payment->currency_id = $client->company->settings->currency_id; $payment->currency_id = $client->company->settings->currency_id;
}
} }
$payment->save(); $payment->save();
@ -129,6 +130,7 @@ class PaymentRepository extends BaseRepository
/*Iterate through invoices and apply payments*/ /*Iterate through invoices and apply payments*/
if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) { if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) {
$invoice_totals = array_sum(array_column($data['invoices'], 'amount')); $invoice_totals = array_sum(array_column($data['invoices'], 'amount'));
$invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get(); $invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get();
@ -164,17 +166,17 @@ class PaymentRepository extends BaseRepository
if ($credit) { if ($credit) {
$credit = $credit->service()->markSent()->save(); $credit = $credit->service()->markSent()->save();
ApplyCreditPayment::dispatchSync($credit, $payment, $paid_credit['amount'], $credit->company); ApplyCreditPayment::dispatchNow($credit, $payment, $paid_credit['amount'], $credit->company);
} }
} }
} }
if ( ! $is_existing_payment && ! $this->import_mode ) { if ( ! $is_existing_payment && ! $this->import_mode ) {
if (array_key_exists('email_receipt', $data) && $data['email_receipt'] == 'true') {
if (array_key_exists('email_receipt', $data) && $data['email_receipt'] == 'true')
$payment->service()->sendEmail(); $payment->service()->sendEmail();
} elseif (! array_key_exists('email_receipt', $data) && $payment->client->getSetting('client_manual_payment_notification')) { elseif(!array_key_exists('email_receipt', $data) && $payment->client->getSetting('client_manual_payment_notification'))
$payment->service()->sendEmail(); $payment->service()->sendEmail();
}
event( new PaymentWasCreated( $payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null) ) ); event( new PaymentWasCreated( $payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null) ) );
} }
@ -205,6 +207,7 @@ class PaymentRepository extends BaseRepository
*/ */
public function processExchangeRates($data, $payment) public function processExchangeRates($data, $payment)
{ {
if(array_key_exists('exchange_rate', $data) && isset($data['exchange_rate']) && $data['exchange_rate'] != 1){ if(array_key_exists('exchange_rate', $data) && isset($data['exchange_rate']) && $data['exchange_rate'] != 1){
return $payment; return $payment;
} }
@ -215,6 +218,7 @@ class PaymentRepository extends BaseRepository
$company_currency = $client->company->settings->currency_id; $company_currency = $client->company->settings->currency_id;
if ($company_currency != $client_currency) { if ($company_currency != $client_currency) {
$exchange_rate = new CurrencyApi(); $exchange_rate = new CurrencyApi();
$payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date)); $payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date));

View File

@ -27,6 +27,8 @@ class SubscriptionRepository extends BaseRepository
{ {
use CleanLineItems; use CleanLineItems;
public int $quantity = 1;
public function save($data, Subscription $subscription): ?Subscription public function save($data, Subscription $subscription): ?Subscription
{ {
$subscription->fill($data); $subscription->fill($data);
@ -119,7 +121,7 @@ class SubscriptionRepository extends BaseRepository
private function makeLineItem($product, $multiplier) private function makeLineItem($product, $multiplier)
{ {
$item = new InvoiceItem; $item = new InvoiceItem;
$item->quantity = $product->quantity; $item->quantity = $this->quantity;
$item->product_key = $product->product_key; $item->product_key = $product->product_key;
$item->notes = $product->notes; $item->notes = $product->notes;
$item->cost = $product->price * $multiplier; $item->cost = $product->price * $multiplier;

View File

@ -32,7 +32,7 @@ class VendorContactRepository extends BaseRepository
} }
/* Get array of IDs which have been removed from the contacts array and soft delete each contact */ /* Get array of IDs which have been removed from the contacts array and soft delete each contact */
$vendor->contacts->pluck('id')->diff($contacts->pluck('id'))->each(function ($contact) { $vendor->contacts->pluck('hashed_id')->diff($contacts->pluck('id'))->each(function ($contact) {
VendorContact::destroy($contact); VendorContact::destroy($contact);
}); });

View File

@ -219,6 +219,7 @@ class Statement
protected function getInvoices(): \Illuminate\Support\LazyCollection protected function getInvoices(): \Illuminate\Support\LazyCollection
{ {
return Invoice::withTrashed() return Invoice::withTrashed()
->with('payments.type')
->where('is_deleted', false) ->where('is_deleted', false)
->where('company_id', $this->client->company_id) ->where('company_id', $this->client->company_id)
->where('client_id', $this->client->id) ->where('client_id', $this->client->id)

View File

@ -248,6 +248,7 @@ class InstantPayment
'tokens' => $tokens, 'tokens' => $tokens,
'payment_method_id' => $payment_method_id, 'payment_method_id' => $payment_method_id,
'amount_with_fee' => $invoice_totals + $fee_totals, 'amount_with_fee' => $invoice_totals + $fee_totals,
'client' => $client,
]; ];
if ($is_credit_payment || $totals <= 0) { if ($is_credit_payment || $totals <= 0) {

View File

@ -117,7 +117,7 @@ class AddGatewayFee extends AbstractService
$this->invoice $this->invoice
->ledger() ->ledger()
->updateInvoiceBalance($adjustment, 'Adjustment for removing gateway fee'); ->updateInvoiceBalance($adjustment, 'Adjustment for adding gateway fee');
} }
return $this->invoice; return $this->invoice;
@ -164,7 +164,7 @@ class AddGatewayFee extends AbstractService
$this->invoice $this->invoice
->ledger() ->ledger()
->updateInvoiceBalance($adjustment * -1, 'Adjustment for removing gateway fee'); ->updateInvoiceBalance($adjustment * -1, 'Adjustment for adding gateway fee');
} }
return $this->invoice; return $this->invoice;

View File

@ -43,12 +43,10 @@ class ApplyNumber extends AbstractService
switch ($this->client->getSetting('counter_number_applied')) { switch ($this->client->getSetting('counter_number_applied')) {
case 'when_saved': case 'when_saved':
$this->trySaving(); $this->trySaving();
// $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id);
break; break;
case 'when_sent': case 'when_sent':
if ($this->invoice->status_id == Invoice::STATUS_SENT) { if ($this->invoice->status_id == Invoice::STATUS_SENT) {
$this->trySaving(); $this->trySaving();
// $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id);
} }
break; break;
@ -61,21 +59,30 @@ class ApplyNumber extends AbstractService
private function trySaving() private function trySaving()
{ {
$x=1; $x=1;
do{ do{
try{ try{
$this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id); $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id);
$this->invoice->saveQuietly(); $this->invoice->saveQuietly();
$this->completed = false; $this->completed = false;
} catch (QueryException $e) {
}
catch(QueryException $e){
$x++; $x++;
if ($x > 10) { if($x>10)
$this->completed = false; $this->completed = false;
} }
} }
} while ($this->completed); while($this->completed);
} }
} }

View File

@ -14,6 +14,7 @@ namespace App\Services\Payment;
use App\Events\Invoice\InvoiceWasUpdated; use App\Events\Invoice\InvoiceWasUpdated;
use App\Jobs\Invoice\InvoiceWorkflowSettings; use App\Jobs\Invoice\InvoiceWorkflowSettings;
use App\Jobs\Ninja\TransactionLog; use App\Jobs\Ninja\TransactionLog;
use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash; use App\Models\PaymentHash;
@ -43,20 +44,17 @@ class UpdateInvoicePayment
$client = $this->payment->client; $client = $this->payment->client;
if ($client->trashed()) { if($client->trashed())
$client->restore(); $client->restore();
}
collect($paid_invoices)->each(function ($paid_invoice) use ($invoices, $client) { collect($paid_invoices)->each(function ($paid_invoice) use ($invoices, $client) {
$client = $client->fresh();
$invoice = $invoices->first(function ($inv) use ($paid_invoice) { $invoice = $invoices->first(function ($inv) use ($paid_invoice) {
return $paid_invoice->invoice_id == $inv->hashed_id; return $paid_invoice->invoice_id == $inv->hashed_id;
}); });
if ($invoice->trashed()) { if($invoice->trashed())
$invoice->restore(); $invoice->restore();
}
if ($invoice->id == $this->payment_hash->fee_invoice_id) { if ($invoice->id == $this->payment_hash->fee_invoice_id) {
$paid_amount = $paid_invoice->amount + $this->payment_hash->fee_total; $paid_amount = $paid_invoice->amount + $this->payment_hash->fee_total;
@ -64,14 +62,19 @@ class UpdateInvoicePayment
$paid_amount = $paid_invoice->amount; $paid_amount = $paid_invoice->amount;
} }
$client->paid_to_date += $paid_amount; \DB::connection(config('database.default'))->transaction(function () use($client, $paid_amount){
$client->balance -= $paid_amount;
$client->save(); $update_client = Client::withTrashed()->where('id', $client->id)->lockForUpdate()->first();
$update_client->paid_to_date += $paid_amount;
$update_client->balance -= $paid_amount;
$update_client->save();
}, 1);
/* Need to determine here is we have an OVER payment - if YES only apply the max invoice amount */ /* Need to determine here is we have an OVER payment - if YES only apply the max invoice amount */
if ($paid_amount > $invoice->partial && $paid_amount > $invoice->balance) { if($paid_amount > $invoice->partial && $paid_amount > $invoice->balance)
$paid_amount = $invoice->balance; $paid_amount = $invoice->balance;
}
/*Improve performance here - 26-01-2022 - also change the order of events for invoice first*/ /*Improve performance here - 26-01-2022 - also change the order of events for invoice first*/
//caution what if we amount paid was less than partial - we wipe it! //caution what if we amount paid was less than partial - we wipe it!
@ -115,6 +118,8 @@ class UpdateInvoicePayment
]; ];
TransactionLog::dispatch(TransactionEvent::GATEWAY_PAYMENT_MADE, $transaction, $invoice->company->db); TransactionLog::dispatch(TransactionEvent::GATEWAY_PAYMENT_MADE, $transaction, $invoice->company->db);
}); });
/* Remove the event updater from within the loop to prevent race conditions */ /* Remove the event updater from within the loop to prevent race conditions */
@ -122,7 +127,9 @@ class UpdateInvoicePayment
$this->payment->saveQuietly(); $this->payment->saveQuietly();
$invoices->each(function ($invoice) { $invoices->each(function ($invoice) {
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
}); });
return $this->payment->fresh(); return $this->payment->fresh();

View File

@ -60,40 +60,33 @@ class Design extends BaseDesign
public $company; public $company;
public $client_or_vendor_entity;
/** @var array */ /** @var array */
public $aging = []; public $aging = [];
const BOLD = 'bold'; const BOLD = 'bold';
const BUSINESS = 'business'; const BUSINESS = 'business';
const CLEAN = 'clean'; const CLEAN = 'clean';
const CREATIVE = 'creative'; const CREATIVE = 'creative';
const ELEGANT = 'elegant'; const ELEGANT = 'elegant';
const HIPSTER = 'hipster'; const HIPSTER = 'hipster';
const MODERN = 'modern'; const MODERN = 'modern';
const PLAIN = 'plain'; const PLAIN = 'plain';
const PLAYFUL = 'playful'; const PLAYFUL = 'playful';
const CUSTOM = 'custom'; const CUSTOM = 'custom';
const DELIVERY_NOTE = 'delivery_note'; const DELIVERY_NOTE = 'delivery_note';
const STATEMENT = 'statement'; const STATEMENT = 'statement';
const PURCHASE_ORDER = 'purchase_order'; const PURCHASE_ORDER = 'purchase_order';
public function __construct(string $design = null, array $options = []) public function __construct(string $design = null, array $options = [])
{ {
Str::endsWith('.html', $design) ? $this->design = $design : $this->design = "{$design}.html"; Str::endsWith('.html', $design) ? $this->design = $design : $this->design = "{$design}.html";
$this->options = $options; $this->options = $options;
} }
public function html(): ?string public function html(): ?string
@ -182,9 +175,27 @@ class Design extends BaseDesign
$this->sharedFooterElements(), $this->sharedFooterElements(),
], ],
], ],
// 'swiss-qr' => [
// 'id' => 'swiss-qr',
// 'elements' => $this->swissQrCodeElement(),
// ]
]; ];
} }
public function swissQrCodeElement() :array
{
if($this->type == self::DELIVERY_NOTE)
return [];
$elements = [];
if(strlen($this->company->getSetting('qr_iban')) > 5 && strlen($this->company->getSetting('besr_id')) > 1)
$elements[] = ['element' => 'qr_code', 'content' => '$swiss_qr', 'show_empty' => false, 'properties' => ['data-ref' => 'swiss-qr-code']];
return $elements;
}
public function companyDetails(): array public function companyDetails(): array
{ {
$variables = $this->context['pdf_variables']['company_details']; $variables = $this->context['pdf_variables']['company_details'];
@ -215,9 +226,8 @@ class Design extends BaseDesign
{ {
$elements = []; $elements = [];
if (! $this->vendor) { if(!$this->vendor)
return $elements; return $elements;
}
$variables = $this->context['pdf_variables']['vendor_details']; $variables = $this->context['pdf_variables']['vendor_details'];
@ -232,9 +242,8 @@ class Design extends BaseDesign
{ {
$elements = []; $elements = [];
if (! $this->client) { if(!$this->client)
return $elements; return $elements;
}
if ($this->type == self::DELIVERY_NOTE) { if ($this->type == self::DELIVERY_NOTE) {
$elements = [ $elements = [
@ -247,7 +256,7 @@ class Design extends BaseDesign
['element' => 'span', 'content' => "{$this->client->shipping_state} ", 'properties' => ['ref' => 'delivery_note-client.shipping_state']], ['element' => 'span', 'content' => "{$this->client->shipping_state} ", 'properties' => ['ref' => 'delivery_note-client.shipping_state']],
['element' => 'span', 'content' => "{$this->client->shipping_postal_code} ", 'properties' => ['ref' => 'delivery_note-client.shipping_postal_code']], ['element' => 'span', 'content' => "{$this->client->shipping_postal_code} ", 'properties' => ['ref' => 'delivery_note-client.shipping_postal_code']],
]], ]],
['element' => 'p', 'content' => $this->client->shipping_country?->name, 'show_empty' => false], ['element' => 'p', 'content' => optional($this->client->shipping_country)->name, 'show_empty' => false],
]; ];
if (!is_null($this->context['contact'])) { if (!is_null($this->context['contact'])) {
@ -268,13 +277,16 @@ class Design extends BaseDesign
public function entityDetails(): array public function entityDetails(): array
{ {
if ($this->type === 'statement') { if ($this->type === 'statement') {
$s_date = $this->translateDate(now(), $this->client->date_format(), $this->client->locale()); $s_date = $this->translateDate(now(), $this->client->date_format(), $this->client->locale());
return [ return [
['element' => 'tr', 'properties' => ['data-ref' => 'statement-label'], 'elements' => [ ['element' => 'tr', 'properties' => ['data-ref' => 'statement-label'], 'elements' => [
['element' => 'th', 'properties' => [], 'content' => ''], ['element' => 'th', 'properties' => [], 'content' => ""],
['element' => 'th', 'properties' => [], 'content' => '<h2>'.ctrans('texts.statement').'</h2>'], ['element' => 'th', 'properties' => [], 'content' => "<h2>".ctrans('texts.statement')."</h2>"],
]], ]],
['element' => 'tr', 'properties' => [], 'elements' => [ ['element' => 'tr', 'properties' => [], 'elements' => [
['element' => 'th', 'properties' => [], 'content' => ctrans('texts.statement_date')], ['element' => 'th', 'properties' => [], 'content' => ctrans('texts.statement_date')],
@ -302,7 +314,9 @@ class Design extends BaseDesign
} }
if($this->vendor){ if($this->vendor){
$variables = $this->context['pdf_variables']['purchase_order_details']; $variables = $this->context['pdf_variables']['purchase_order_details'];
} }
$elements = []; $elements = [];
@ -318,7 +332,10 @@ class Design extends BaseDesign
$_variable = explode('.', $variable)[1]; $_variable = explode('.', $variable)[1];
$_customs = ['custom1', 'custom2', 'custom3', 'custom4']; $_customs = ['custom1', 'custom2', 'custom3', 'custom4'];
if (in_array($_variable, $_customs)) { /* 2/7/2022 don't show custom values if they are empty */
$var = str_replace("custom", "custom_value", $_variable);
if (in_array($_variable, $_customs) && !empty($this->entity->{$var})) {
$elements[] = ['element' => 'tr', 'elements' => [ $elements[] = ['element' => 'tr', 'elements' => [
['element' => 'th', 'content' => $variable . '_label', 'properties' => ['data-ref' => 'entity_details-' . substr($variable, 1) . '_label']], ['element' => 'th', 'content' => $variable . '_label', 'properties' => ['data-ref' => 'entity_details-' . substr($variable, 1) . '_label']],
['element' => 'th', 'content' => $variable, 'properties' => ['data-ref' => 'entity_details-' . substr($variable, 1)]], ['element' => 'th', 'content' => $variable, 'properties' => ['data-ref' => 'entity_details-' . substr($variable, 1)]],
@ -495,12 +512,13 @@ class Design extends BaseDesign
// } // }
// } // }
//24-03-2022 show payments per invoice //24-03-2022 show payments per invoice
foreach ($this->invoices as $invoice) { foreach ($this->invoices as $invoice) {
foreach ($invoice->payments as $payment) { foreach ($invoice->payments as $payment) {
if ($payment->is_deleted) {
if($payment->is_deleted)
continue; continue;
}
$element = ['element' => 'tr', 'elements' => []]; $element = ['element' => 'tr', 'elements' => []];
@ -510,6 +528,7 @@ class Design extends BaseDesign
$element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->pivot->amount, $this->client) ?: '&nbsp;']; $element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->pivot->amount, $this->client) ?: '&nbsp;'];
$tbody[] = $element; $tbody[] = $element;
} }
} }
@ -730,12 +749,13 @@ class Design extends BaseDesign
$variables = $this->context['pdf_variables']['total_columns']; $variables = $this->context['pdf_variables']['total_columns'];
$elements = [ $elements = [
['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [ ['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [
['element' => 'p', 'content' => strtr(str_replace(['labels', 'values'], ['', ''], $_variables['values']['$entity.public_notes']), $_variables), 'properties' => ['data-ref' => 'total_table-public_notes', 'style' => 'text-align: left;']], ['element' => 'p', 'content' => strtr(str_replace(["labels","values"], ["",""], $_variables['values']['$entity.public_notes']), $_variables), 'properties' => ['data-ref' => 'total_table-public_notes', 'style' => 'text-align: left;']],
['element' => 'p', 'content' => '', 'properties' => ['style' => 'text-align: left; display: flex; flex-direction: column; page-break-inside: auto;'], 'elements' => [ ['element' => 'p', 'content' => '', 'properties' => ['style' => 'text-align: left; display: flex; flex-direction: column; page-break-inside: auto;'], 'elements' => [
['element' => 'span', 'content' => '$entity.terms_label: ', 'properties' => ['hidden' => $this->entityVariableCheck('$entity.terms'), 'data-ref' => 'total_table-terms-label', 'style' => 'font-weight: bold; text-align: left; margin-top: 1rem;']], ['element' => 'span', 'content' => '$entity.terms_label: ', 'properties' => ['hidden' => $this->entityVariableCheck('$entity.terms'), 'data-ref' => 'total_table-terms-label', 'style' => 'font-weight: bold; text-align: left; margin-top: 1rem;']],
['element' => 'span', 'content' => strtr(str_replace('labels', '', $_variables['values']['$entity.terms']), $_variables['labels']), 'properties' => ['data-ref' => 'total_table-terms', 'style' => 'text-align: left;']], ['element' => 'span', 'content' => strtr(str_replace("labels", "", $_variables['values']['$entity.terms']), $_variables['labels']), 'properties' => ['data-ref' => 'total_table-terms', 'style' => 'text-align: left;']],
]], ]],
['element' => 'img', 'properties' => ['style' => 'max-width: 50%; height: auto;', 'src' => '$contact.signature', 'id' => 'contact-signature']], ['element' => 'img', 'properties' => ['style' => 'max-width: 50%; height: auto;', 'src' => '$contact.signature', 'id' => 'contact-signature']],
['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: flex; align-items: flex-start; page-break-inside: auto;'], 'elements' => [ ['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: flex; align-items: flex-start; page-break-inside: auto;'], 'elements' => [
@ -745,6 +765,7 @@ class Design extends BaseDesign
['element' => 'div', 'properties' => ['class' => 'totals-table-right-side', 'dir' => '$dir'], 'elements' => []], ['element' => 'div', 'properties' => ['class' => 'totals-table-right-side', 'dir' => '$dir'], 'elements' => []],
]; ];
if ($this->type == self::DELIVERY_NOTE) { if ($this->type == self::DELIVERY_NOTE) {
return $elements; return $elements;
} }
@ -760,6 +781,14 @@ class Design extends BaseDesign
} }
} }
if ($this->entity instanceof Credit) {
// We don't want to show Balanace due on the quotes.
if (in_array('$paid_to_date', $variables)) {
$variables = \array_diff($variables, ['$paid_to_date']);
}
}
foreach (['discount'] as $property) { foreach (['discount'] as $property) {
$variable = sprintf('%s%s', '$', $property); $variable = sprintf('%s%s', '$', $property);
@ -787,7 +816,7 @@ class Design extends BaseDesign
foreach ($taxes as $i => $tax) { foreach ($taxes as $i => $tax) {
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [ $elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content', 'content' => $tax['name'], 'properties' => ['data-ref' => 'totals-table-total_tax_' . $i . '-label']], ['element' => 'span', 'content', 'content' => $tax['name'], 'properties' => ['data-ref' => 'totals-table-total_tax_' . $i . '-label']],
['element' => 'span', 'content', 'content' => Number::formatMoney($tax['total'], $this->context['client']), 'properties' => ['data-ref' => 'totals-table-total_tax_'.$i]], ['element' => 'span', 'content', 'content' => Number::formatMoney($tax['total'], $this->client_or_vendor_entity), 'properties' => ['data-ref' => 'totals-table-total_tax_' . $i]],
]]; ]];
} }
} elseif ($variable == '$line_taxes') { } elseif ($variable == '$line_taxes') {
@ -800,13 +829,13 @@ class Design extends BaseDesign
foreach ($taxes as $i => $tax) { foreach ($taxes as $i => $tax) {
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [ $elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content', 'content' => $tax['name'], 'properties' => ['data-ref' => 'totals-table-line_tax_' . $i . '-label']], ['element' => 'span', 'content', 'content' => $tax['name'], 'properties' => ['data-ref' => 'totals-table-line_tax_' . $i . '-label']],
['element' => 'span', 'content', 'content' => Number::formatMoney($tax['total'], $this->context['client']), 'properties' => ['data-ref' => 'totals-table-line_tax_'.$i]], ['element' => 'span', 'content', 'content' => Number::formatMoney($tax['total'], $this->client_or_vendor_entity), 'properties' => ['data-ref' => 'totals-table-line_tax_' . $i]],
]]; ]];
} }
} elseif (Str::startsWith($variable, '$custom_surcharge')) { } elseif (Str::startsWith($variable, '$custom_surcharge')) {
$_variable = ltrim($variable, '$'); // $custom_surcharge1 -> custom_surcharge1 $_variable = ltrim($variable, '$'); // $custom_surcharge1 -> custom_surcharge1
$visible = (int) $this->entity->{$_variable} != 0 || $this->entity->{$_variable} != '0' || ! $this->entity->{$_variable}; $visible = (int)$this->entity->{$_variable} > 0 || (int)$this->entity->{$_variable} < 0 || !$this->entity->{$_variable};
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [ $elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']], ['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],
@ -829,7 +858,7 @@ class Design extends BaseDesign
} }
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [ $elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => ''], ['element' => 'span', 'content' => '',],
['element' => 'span', 'content' => ''], ['element' => 'span', 'content' => ''],
]]; ]];

View File

@ -32,10 +32,12 @@ trait DesignHelpers
if (isset($this->context['vendor'])) { if (isset($this->context['vendor'])) {
$this->vendor = $this->context['vendor']; $this->vendor = $this->context['vendor'];
$this->client_or_vendor_entity = $this->context['vendor'];
} }
if (isset($this->context['client'])) { if (isset($this->context['client'])) {
$this->client = $this->context['client']; $this->client = $this->context['client'];
$this->client_or_vendor_entity = $this->context['client'];
} }
if (isset($this->context['entity'])) { if (isset($this->context['entity'])) {

View File

@ -40,7 +40,20 @@ class ApplyNumber extends AbstractService
return $this->purchase_order; return $this->purchase_order;
} }
switch ($this->vendor->company->getSetting('counter_number_applied')) {
case 'when_saved':
$this->trySaving(); $this->trySaving();
break;
case 'when_sent':
if ($this->invoice->status_id == PurchaseOrder::STATUS_SENT) {
$this->trySaving();
}
break;
default:
break;
}
return $this->purchase_order; return $this->purchase_order;
} }

View File

@ -0,0 +1,69 @@
<?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\Services\PurchaseOrder;
use App\Factory\ExpenseFactory;
use App\Models\PurchaseOrder;
use App\Utils\Traits\GeneratesCounter;
class PurchaseOrderExpense
{
use GeneratesCounter;
private PurchaseOrder $purchase_order;
public function __construct(PurchaseOrder $purchase_order)
{
$this->purchase_order = $purchase_order;
}
public function run()
{
$expense = ExpenseFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id);
$expense->amount = $this->purchase_order->uses_inclusive_taxes ? $this->purchase_order->amount : ($this->purchase_order->amount - $this->purchase_order->total_taxes);
$expense->date = now();
$expense->vendor_id = $this->purchase_order->vendor_id;
$expense->public_notes = $this->purchase_order->public_notes;
$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;
$line_items = $this->purchase_order->line_items;
$expense->public_notes = '';
foreach($line_items as $line_item){
$expense->public_notes .= $line_item->quantity . " x " . $line_item->product_key. " [ " .$line_item->notes . " ]\n";
}
$tax_map = $this->purchase_order->calc()->getTaxMap();
if($this->purchase_order->total_taxes > 0)
{
$expense->tax_amount1 = $this->purchase_order->total_taxes;
$expense->tax_name1 = ctrans("texts.tax");
}
$expense->number = empty($expense->number) ? $this->getNextExpenseNumber($expense) : $expense->number;
$expense->save();
$this->purchase_order->expense_id = $expense->id;
$this->purchase_order->save();
return $expense;
}
}

View File

@ -0,0 +1,57 @@
<?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\Services\PurchaseOrder;
use App\Factory\ExpenseFactory;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Admin\InventoryNotificationObject;
use App\Models\Product;
use App\Models\PurchaseOrder;
class PurchaseOrderInventory
{
private PurchaseOrder $purchase_order;
public function __construct(PurchaseOrder $purchase_order)
{
$this->purchase_order = $purchase_order;
}
public function run()
{
$line_items = $this->purchase_order->line_items;
foreach($line_items as $item)
{
$p = Product::where('product_key', $item->product_key)->where('company_id', $this->purchase_order->company_id)->first();
if(!$p)
continue;
$p->in_stock_quantity += $item->quantity;
$p->saveQuietly();
}
$this->purchase_order->status_id = PurchaseOrder::STATUS_RECEIVED;
$this->purchase_order->save();
return $this->purchase_order;
}
}

View File

@ -16,6 +16,7 @@ use App\Models\PurchaseOrder;
use App\Services\PurchaseOrder\ApplyNumber; use App\Services\PurchaseOrder\ApplyNumber;
use App\Services\PurchaseOrder\CreateInvitations; use App\Services\PurchaseOrder\CreateInvitations;
use App\Services\PurchaseOrder\GetPurchaseOrderPdf; use App\Services\PurchaseOrder\GetPurchaseOrderPdf;
use App\Services\PurchaseOrder\PurchaseOrderExpense;
use App\Services\PurchaseOrder\TriggeredActions; use App\Services\PurchaseOrder\TriggeredActions;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -32,6 +33,7 @@ class PurchaseOrderService
public function createInvitations() public function createInvitations()
{ {
$this->purchase_order = (new CreateInvitations($this->purchase_order))->run(); $this->purchase_order = (new CreateInvitations($this->purchase_order))->run();
return $this; return $this;
@ -39,25 +41,34 @@ class PurchaseOrderService
public function applyNumber() public function applyNumber()
{ {
$this->invoice = (new ApplyNumber($this->purchase_order->vendor, $this->purchase_order))->run(); $this->purchase_order = (new ApplyNumber($this->purchase_order->vendor, $this->purchase_order))->run();
return $this; return $this;
} }
public function fillDefaults() public function fillDefaults()
{ {
// $settings = $this->purchase_order->client->getMergedSettings();
// //TODO implement design, footer, terms $settings = $this->purchase_order->company->settings;
// /* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/ if (! $this->purchase_order->design_id)
// if (!isset($this->purchase_order->exchange_rate) && $this->purchase_order->client->currency()->id != (int)$this->purchase_order->company->settings->currency_id) $this->purchase_order->design_id = $this->decodePrimaryKey($settings->invoice_design_id);
// $this->purchase_order->exchange_rate = $this->purchase_order->client->currency()->exchange_rate;
// if (!isset($this->purchase_order->public_notes)) if (!isset($this->invoice->footer) || empty($this->invoice->footer))
// $this->purchase_order->public_notes = $this->purchase_order->client->public_notes; $this->purchase_order->footer = $settings->purchase_order_footer;
if (!isset($this->purchase_order->terms) || empty($this->purchase_order->terms))
$this->purchase_order->terms = $settings->purchase_order_terms;
if (!isset($this->purchase_order->public_notes) || empty($this->purchase_order->public_notes))
$this->purchase_order->public_notes = $this->purchase_order->vendor->public_notes;
if($settings->counter_number_applied == 'when_saved'){
$this->applyNumber()->save();
}
return $this; return $this;
} }
public function triggeredActions($request) public function triggeredActions($request)
@ -89,9 +100,11 @@ class PurchaseOrderService
public function touchPdf($force = false) public function touchPdf($force = false)
{ {
try { try {
if($force){ if($force){
$this->purchase_order->invitations->each(function ($invitation) { $this->purchase_order->invitations->each(function ($invitation) {
CreatePurchaseOrderPdf::dispatchSync($invitation); CreatePurchaseOrderPdf::dispatchNow($invitation);
}); });
return $this; return $this;
@ -100,13 +113,39 @@ class PurchaseOrderService
$this->purchase_order->invitations->each(function ($invitation) { $this->purchase_order->invitations->each(function ($invitation) {
CreatePurchaseOrderPdf::dispatch($invitation); CreatePurchaseOrderPdf::dispatch($invitation);
}); });
} catch (\Exception $e) {
nlog('failed creating purchase orders in Touch PDF'); }
catch(\Exception $e){
nlog("failed creating purchase orders in Touch PDF");
} }
return $this; return $this;
} }
public function add_to_inventory()
{
if($this->purchase_order->status_id >= PurchaseOrder::STATUS_RECEIVED)
return $this->purchase_order;
$this->purchase_order = (new PurchaseOrderInventory($this->purchase_order))->run();
return $this;
}
public function expense()
{
$this->markSent();
if($this->purchase_order->expense()->exists())
return $this;
$expense = (new PurchaseOrderExpense($this->purchase_order))->run();
return $expense;
}
/** /**
* Saves the purchase order. * Saves the purchase order.
* @return \App\Models\PurchaseOrder object * @return \App\Models\PurchaseOrder object
@ -117,4 +156,5 @@ class PurchaseOrderService
return $this->purchase_order; return $this->purchase_order;
} }
} }

View File

@ -11,6 +11,7 @@
namespace App\Services\Recurring; namespace App\Services\Recurring;
use App\Jobs\RecurringInvoice\SendRecurring;
use App\Jobs\Util\UnlinkFile; use App\Jobs\Util\UnlinkFile;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Services\Recurring\GetInvoicePdf; use App\Services\Recurring\GetInvoicePdf;
@ -34,9 +35,8 @@ class RecurringService
*/ */
public function stop() public function stop()
{ {
if ($this->recurring_entity->status_id < RecurringInvoice::STATUS_PAUSED) { if($this->recurring_entity->status_id < RecurringInvoice::STATUS_PAUSED)
$this->recurring_entity->status_id = RecurringInvoice::STATUS_PAUSED; $this->recurring_entity->status_id = RecurringInvoice::STATUS_PAUSED;
}
return $this; return $this;
} }
@ -50,6 +50,7 @@ class RecurringService
public function start() public function start()
{ {
if ($this->recurring_entity->remaining_cycles == 0) { if ($this->recurring_entity->remaining_cycles == 0) {
return $this; return $this;
} }
@ -84,15 +85,20 @@ class RecurringService
public function deletePdf() public function deletePdf()
{ {
$this->recurring_entity->invitations->each(function ($invitation){ $this->recurring_entity->invitations->each(function ($invitation){
UnlinkFile::dispatchSync(config('filesystems.default'), $this->recurring_entity->client->recurring_invoice_filepath($invitation).$this->recurring_entity->numberFormatter().'.pdf');
UnlinkFile::dispatchNow(config('filesystems.default'), $this->recurring_entity->client->recurring_invoice_filepath($invitation) . $this->recurring_entity->numberFormatter().'.pdf');
}); });
return $this; return $this;
} }
public function triggeredActions($request) public function triggeredActions($request)
{ {
if ($request->has('start') && $request->input('start') == 'true') { if ($request->has('start') && $request->input('start') == 'true') {
$this->start(); $this->start();
} }
@ -101,7 +107,12 @@ class RecurringService
$this->stop(); $this->stop();
} }
if (isset($this->recurring_entity->client)) { if ($request->has('send_now') && $request->input('send_now') == 'true' && $this->recurring_entity->invoices()->count() == 0) {
$this->sendNow();
}
if(isset($this->recurring_entity->client))
{
$offset = $this->recurring_entity->client->timezone_offset(); $offset = $this->recurring_entity->client->timezone_offset();
$this->recurring_entity->next_send_date = Carbon::parse($this->recurring_entity->next_send_date_client)->startOfDay()->addSeconds($offset); $this->recurring_entity->next_send_date = Carbon::parse($this->recurring_entity->next_send_date_client)->startOfDay()->addSeconds($offset);
} }
@ -109,8 +120,21 @@ class RecurringService
return $this; return $this;
} }
public function sendNow()
{
if($this->recurring_entity instanceof RecurringInvoice && $this->recurring_entity->status_id == RecurringInvoice::STATUS_DRAFT){
$this->start()->save();
SendRecurring::dispatchNow($this->recurring_entity, $this->recurring_entity->company->db);
}
return $this->recurring_entity;
}
public function fillDefaults() public function fillDefaults()
{ {
return $this; return $this;
} }

View File

@ -67,8 +67,9 @@ class SubscriptionService
*/ */
public function completePurchase(PaymentHash $payment_hash) public function completePurchase(PaymentHash $payment_hash)
{ {
if (!property_exists($payment_hash->data, 'billing_context')) { if (!property_exists($payment_hash->data, 'billing_context')) {
throw new \Exception('Illegal entrypoint into method, payload must contain billing context'); throw new \Exception("Illegal entrypoint into method, payload must contain billing context");
} }
if($payment_hash->data->billing_context->context == 'change_plan') { if($payment_hash->data->billing_context->context == 'change_plan') {
@ -77,6 +78,7 @@ class SubscriptionService
// if we have a recurring product - then generate a recurring invoice // if we have a recurring product - then generate a recurring invoice
if(strlen($this->subscription->recurring_product_ids) >=1){ if(strlen($this->subscription->recurring_product_ids) >=1){
$recurring_invoice = $this->convertInvoiceToRecurring($payment_hash->payment->client_id); $recurring_invoice = $this->convertInvoiceToRecurring($payment_hash->payment->client_id);
$recurring_invoice_repo = new RecurringInvoiceRepository(); $recurring_invoice_repo = new RecurringInvoiceRepository();
@ -102,7 +104,10 @@ class SubscriptionService
$response = $this->triggerWebhook($context); $response = $this->triggerWebhook($context);
$this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
} else {
}
else
{
$invoice = Invoice::withTrashed()->find($payment_hash->fee_invoice_id); $invoice = Invoice::withTrashed()->find($payment_hash->fee_invoice_id);
$context = [ $context = [
@ -118,9 +123,9 @@ class SubscriptionService
/* 06-04-2022 */ /* 06-04-2022 */
/* We may not be in a state where the user is present */ /* We may not be in a state where the user is present */
if (auth()->guard('contact')) { if(auth()->guard('contact'))
$this->handleRedirect('/client/invoices/'.$this->encodePrimaryKey($payment_hash->fee_invoice_id)); $this->handleRedirect('/client/invoices/'.$this->encodePrimaryKey($payment_hash->fee_invoice_id));
}
} }
} }
@ -151,9 +156,8 @@ class SubscriptionService
// Redirects from here work just fine. Livewire will respect it. // Redirects from here work just fine. Livewire will respect it.
$client_contact = ClientContact::find($data['contact_id']); $client_contact = ClientContact::find($data['contact_id']);
if (! $this->subscription->trial_enabled) { if(!$this->subscription->trial_enabled)
return new \Exception('Trials are disabled for this product'); return new \Exception("Trials are disabled for this product");
}
//create recurring invoice with start date = trial_duration + 1 day //create recurring invoice with start date = trial_duration + 1 day
$recurring_invoice_repo = new RecurringInvoiceRepository(); $recurring_invoice_repo = new RecurringInvoiceRepository();
@ -163,13 +167,16 @@ class SubscriptionService
$recurring_invoice->next_send_date_client = now()->addSeconds($this->subscription->trial_duration); $recurring_invoice->next_send_date_client = now()->addSeconds($this->subscription->trial_duration);
$recurring_invoice->backup = 'is_trial'; $recurring_invoice->backup = 'is_trial';
if (array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { if(array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0)
$recurring_invoice->discount = $this->subscription->promo_discount; {
$recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount;
} elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) {
$recurring_invoice->discount = $this->subscription->promo_discount; $recurring_invoice->discount = $this->subscription->promo_discount;
$recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount; $recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount;
} }
elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) {
$recurring_invoice->discount = $this->subscription->promo_discount;
$recurring_invoice->is_amount_discount = $this->subscription->is_amount_discount;
}
$recurring_invoice = $recurring_invoice_repo->save($data, $recurring_invoice); $recurring_invoice = $recurring_invoice_repo->save($data, $recurring_invoice);
@ -225,6 +232,7 @@ class SubscriptionService
//sometimes the last document could be a credit if the user is paying for their service with credits. //sometimes the last document could be a credit if the user is paying for their service with credits.
if(!$outstanding_invoice){ if(!$outstanding_invoice){
$outstanding_invoice = Credit::where('subscription_id', $this->subscription->id) $outstanding_invoice = Credit::where('subscription_id', $this->subscription->id)
->where('client_id', $recurring_invoice->client_id) ->where('client_id', $recurring_invoice->client_id)
->where('is_deleted', 0) ->where('is_deleted', 0)
@ -236,19 +244,23 @@ class SubscriptionService
//28-02-2022 //28-02-2022
if($recurring_invoice->invoices()->count() == 0){ if($recurring_invoice->invoices()->count() == 0){
return $target->price; return $target->price;
} elseif ($outstanding->count() == 0) { }
elseif ($outstanding->count() == 0){
//nothing outstanding //nothing outstanding
return $target->price - $this->calculateProRataRefundForSubscription($outstanding_invoice); return $target->price - $this->calculateProRataRefundForSubscription($outstanding_invoice);
} elseif ($outstanding->count() == 1) { }
elseif ($outstanding->count() == 1){
//user has multiple amounts outstanding //user has multiple amounts outstanding
return $target->price - $this->calculateProRataRefundForSubscription($outstanding_invoice); return $target->price - $this->calculateProRataRefundForSubscription($outstanding_invoice);
} elseif ($outstanding->count() > 1) { }
elseif ($outstanding->count() > 1) {
//user is changing plan mid frequency cycle //user is changing plan mid frequency cycle
//we cannot handle this if there are more than one invoice outstanding. //we cannot handle this if there are more than one invoice outstanding.
return $target->price; return $target->price;
} }
return $target->price; return $target->price;
} }
/** /**
@ -259,9 +271,8 @@ class SubscriptionService
*/ */
private function calculateProRataRefundForSubscription($invoice) :float private function calculateProRataRefundForSubscription($invoice) :float
{ {
if (! $invoice || ! $invoice->date) { if(!$invoice || !$invoice->date)
return 0; return 0;
}
$start_date = Carbon::parse($invoice->date); $start_date = Carbon::parse($invoice->date);
@ -278,6 +289,7 @@ class SubscriptionService
// nlog("pro rata refund = {$pro_rata_refund}"); // nlog("pro rata refund = {$pro_rata_refund}");
return $pro_rata_refund; return $pro_rata_refund;
} }
/** /**
@ -288,9 +300,8 @@ class SubscriptionService
*/ */
private function calculateProRataRefund($invoice) :float private function calculateProRataRefund($invoice) :float
{ {
if (! $invoice || ! $invoice->date) { if(!$invoice || !$invoice->date)
return 0; return 0;
}
$start_date = Carbon::parse($invoice->date); $start_date = Carbon::parse($invoice->date);
@ -300,9 +311,8 @@ class SubscriptionService
$days_in_frequency = $this->getDaysInFrequency(); $days_in_frequency = $this->getDaysInFrequency();
if ($days_of_subscription_used >= $days_in_frequency) { if($days_of_subscription_used >= $days_in_frequency)
return 0; return 0;
}
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2); $pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2);
@ -311,6 +321,7 @@ class SubscriptionService
// nlog("pro rata refund = {$pro_rata_refund}"); // nlog("pro rata refund = {$pro_rata_refund}");
return $pro_rata_refund; return $pro_rata_refund;
} }
/** /**
@ -323,9 +334,8 @@ class SubscriptionService
*/ */
private function calculateProRataRefundItems($invoice, $is_credit = false) :array private function calculateProRataRefundItems($invoice, $is_credit = false) :array
{ {
if (! $invoice) { if(!$invoice)
return []; return [];
}
/* depending on whether we are creating an invoice or a credit*/ /* depending on whether we are creating an invoice or a credit*/
$multiplier = $is_credit ? 1 : -1; $multiplier = $is_credit ? 1 : -1;
@ -343,17 +353,24 @@ class SubscriptionService
$line_items = []; $line_items = [];
foreach ($invoice->line_items as $item) { foreach($invoice->line_items as $item)
if ($item->product_key != ctrans('texts.refund')) { {
if($item->product_key != ctrans('texts.refund'))
{
$item->cost = ($item->cost*$ratio*$multiplier); $item->cost = ($item->cost*$ratio*$multiplier);
$item->product_key = ctrans('texts.refund'); $item->product_key = ctrans('texts.refund');
$item->notes = ctrans('texts.refund').': '.$item->notes; $item->notes = ctrans('texts.refund') . ": ". $item->notes;
$line_items[] = $item; $line_items[] = $item;
} }
} }
return $line_items; return $line_items;
} }
/** /**
@ -364,6 +381,7 @@ class SubscriptionService
*/ */
private function calculateProRataCharge($invoice) :float private function calculateProRataCharge($invoice) :float
{ {
$start_date = Carbon::parse($invoice->date); $start_date = Carbon::parse($invoice->date);
$current_date = now(); $current_date = now();
@ -406,7 +424,9 @@ class SubscriptionService
if($recurring_invoice->invoices()->count() == 0){ if($recurring_invoice->invoices()->count() == 0){
$pro_rata_refund_amount = 0; $pro_rata_refund_amount = 0;
} elseif (! $last_invoice) { }
elseif(!$last_invoice){
$is_credit = true; $is_credit = true;
$last_invoice = Credit::where('subscription_id', $recurring_invoice->subscription_id) $last_invoice = Credit::where('subscription_id', $recurring_invoice->subscription_id)
@ -417,10 +437,16 @@ class SubscriptionService
->first(); ->first();
$pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription); $pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription);
} elseif ($last_invoice->balance > 0) {
}
elseif($last_invoice->balance > 0)
{
$pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription);
nlog("pro rata charge = {$pro_rata_charge_amount}"); nlog("pro rata charge = {$pro_rata_charge_amount}");
} else { }
else
{
$pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1; $pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1;
nlog("pro rata refund = {$pro_rata_refund_amount}"); nlog("pro rata refund = {$pro_rata_refund_amount}");
} }
@ -432,9 +458,8 @@ class SubscriptionService
$credit = false; $credit = false;
/* Only generate a credit if the previous invoice was paid in full. */ /* Only generate a credit if the previous invoice was paid in full. */
if ($last_invoice && $last_invoice->balance == 0) { if($last_invoice && $last_invoice->balance == 0)
$credit = $this->createCredit($last_invoice, $target_subscription, $is_credit); $credit = $this->createCredit($last_invoice, $target_subscription, $is_credit);
}
$new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice); $new_recurring_invoice = $this->createNewRecurringInvoice($recurring_invoice);
@ -452,15 +477,16 @@ class SubscriptionService
nlog($response); nlog($response);
if ($credit) { if($credit)
return $this->handleRedirect('/client/credits/'.$credit->hashed_id); return $this->handleRedirect('/client/credits/'.$credit->hashed_id);
} else { else
return $this->handleRedirect('/client/credits'); return $this->handleRedirect('/client/credits');
}
} }
public function changePlanPaymentCheck($data) public function changePlanPaymentCheck($data)
{ {
$recurring_invoice = $data['recurring_invoice']; $recurring_invoice = $data['recurring_invoice'];
$old_subscription = $data['subscription']; $old_subscription = $data['subscription'];
$target_subscription = $data['target']; $target_subscription = $data['target'];
@ -474,25 +500,28 @@ class SubscriptionService
->withTrashed() ->withTrashed()
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->first(); ->first();
if (! $last_invoice) { if(!$last_invoice)
return true; return true;
}
if ($last_invoice->balance > 0) { if($last_invoice->balance > 0)
{
$pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription);
nlog("pro rata charge = {$pro_rata_charge_amount}"); nlog("pro rata charge = {$pro_rata_charge_amount}");
} else {
}
else
{
$pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1; $pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1;
nlog("pro rata refund = {$pro_rata_refund_amount}"); nlog("pro rata refund = {$pro_rata_refund_amount}");
} }
$total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price; $total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price;
if ($total_payable > 0) { if($total_payable > 0)
return true; return true;
}
return false; return false;
} }
/** /**
@ -503,6 +532,7 @@ class SubscriptionService
*/ */
public function createChangePlanInvoice($data) public function createChangePlanInvoice($data)
{ {
$recurring_invoice = $data['recurring_invoice']; $recurring_invoice = $data['recurring_invoice'];
$old_subscription = $data['subscription']; $old_subscription = $data['subscription'];
$target_subscription = $data['target']; $target_subscription = $data['target'];
@ -519,10 +549,14 @@ class SubscriptionService
if(!$last_invoice){ if(!$last_invoice){
//do nothing //do nothing
} elseif ($last_invoice->balance > 0) { }
else if($last_invoice->balance > 0)
{
$pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription); $pro_rata_charge_amount = $this->calculateProRataCharge($last_invoice, $old_subscription);
nlog("pro rata charge = {$pro_rata_charge_amount}"); nlog("pro rata charge = {$pro_rata_charge_amount}");
} else { }
else
{
$pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1; $pro_rata_refund_amount = $this->calculateProRataRefund($last_invoice, $old_subscription) * -1;
nlog("pro rata refund = {$pro_rata_refund_amount}"); nlog("pro rata refund = {$pro_rata_refund_amount}");
} }
@ -530,6 +564,7 @@ class SubscriptionService
$total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price; $total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price;
return $this->proRataInvoice($last_invoice, $target_subscription, $recurring_invoice->client_id); return $this->proRataInvoice($last_invoice, $target_subscription, $recurring_invoice->client_id);
} }
/** /**
@ -540,13 +575,12 @@ class SubscriptionService
*/ */
private function handlePlanChange($payment_hash) private function handlePlanChange($payment_hash)
{ {
nlog('handle plan change'); nlog("handle plan change");
$old_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice); $old_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice);
if (! $old_recurring_invoice) { if(!$old_recurring_invoice)
return $this->handleRedirect('/client/recurring_invoices/'); return $this->handleRedirect('/client/recurring_invoices/');
}
$recurring_invoice = $this->createNewRecurringInvoice($old_recurring_invoice); $recurring_invoice = $this->createNewRecurringInvoice($old_recurring_invoice);
@ -560,11 +594,13 @@ class SubscriptionService
'account_key' => $recurring_invoice->client->custom_value2, 'account_key' => $recurring_invoice->client->custom_value2,
]; ];
$response = $this->triggerWebhook($context); $response = $this->triggerWebhook($context);
nlog($response); nlog($response);
return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id); return $this->handleRedirect('/client/recurring_invoices/'.$recurring_invoice->hashed_id);
} }
/** /**
@ -576,6 +612,7 @@ class SubscriptionService
*/ */
public function createNewRecurringInvoice($old_recurring_invoice) :RecurringInvoice public function createNewRecurringInvoice($old_recurring_invoice) :RecurringInvoice
{ {
$old_recurring_invoice->service()->stop()->save(); $old_recurring_invoice->service()->stop()->save();
$recurring_invoice_repo = new RecurringInvoiceRepository(); $recurring_invoice_repo = new RecurringInvoiceRepository();
@ -594,6 +631,7 @@ class SubscriptionService
->save(); ->save();
return $recurring_invoice; return $recurring_invoice;
} }
/** /**
@ -605,6 +643,7 @@ class SubscriptionService
*/ */
private function createCredit($last_invoice, $target, $is_credit = false) private function createCredit($last_invoice, $target, $is_credit = false)
{ {
$last_invoice_is_credit = $is_credit ? false : true; $last_invoice_is_credit = $is_credit ? false : true;
$subscription_repo = new SubscriptionRepository(); $subscription_repo = new SubscriptionRepository();
@ -625,6 +664,7 @@ class SubscriptionService
]; ];
return $credit_repo->save($data, $credit)->service()->markSent()->fillDefaults()->save(); return $credit_repo->save($data, $credit)->service()->markSent()->fillDefaults()->save();
} }
/** /**
@ -657,6 +697,7 @@ class SubscriptionService
->markSent() ->markSent()
->fillDefaults() ->fillDefaults()
->save(); ->save();
} }
/** /**
@ -665,24 +706,30 @@ class SubscriptionService
* @param array $data * @param array $data
* @return Invoice * @return Invoice
*/ */
public function createInvoice($data): ?Invoice public function createInvoice($data, $quantity = 1): ?\App\Models\Invoice
{ {
$invoice_repo = new InvoiceRepository(); $invoice_repo = new InvoiceRepository();
$subscription_repo = new SubscriptionRepository(); $subscription_repo = new SubscriptionRepository();
$subscription_repo->quantity = $quantity;
$invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id); $invoice = InvoiceFactory::create($this->subscription->company_id, $this->subscription->user_id);
$invoice->line_items = $subscription_repo->generateLineItems($this->subscription); $invoice->line_items = $subscription_repo->generateLineItems($this->subscription);
$invoice->subscription_id = $this->subscription->id; $invoice->subscription_id = $this->subscription->id;
if (strlen($data['coupon']) >= 1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { if(strlen($data['coupon']) >=1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0)
{
$invoice->discount = $this->subscription->promo_discount; $invoice->discount = $this->subscription->promo_discount;
$invoice->is_amount_discount = $this->subscription->is_amount_discount; $invoice->is_amount_discount = $this->subscription->is_amount_discount;
} elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) { }
elseif(strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) {
$invoice->discount = $this->subscription->promo_discount; $invoice->discount = $this->subscription->promo_discount;
$invoice->is_amount_discount = $this->subscription->is_amount_discount; $invoice->is_amount_discount = $this->subscription->is_amount_discount;
} }
return $invoice_repo->save($data, $invoice); return $invoice_repo->save($data, $invoice);
} }
/** /**
@ -714,7 +761,6 @@ class SubscriptionService
$recurring_invoice->next_send_date_client = now()->format('Y-m-d'); $recurring_invoice->next_send_date_client = now()->format('Y-m-d');
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
return $recurring_invoice; return $recurring_invoice;
} }
@ -725,6 +771,7 @@ class SubscriptionService
} }
return false; return false;
} }
/** /**
@ -734,13 +781,13 @@ class SubscriptionService
*/ */
public function triggerWebhook($context) public function triggerWebhook($context)
{ {
nlog('trigger webook'); nlog("trigger webook");
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) { 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]; return ["message" => "Success", "status_code" => 200];
} }
nlog('past first if'); nlog("past first if");
$response = false; $response = false;
@ -750,13 +797,18 @@ class SubscriptionService
$response = $this->sendLoad($this->subscription, $body); $response = $this->sendLoad($this->subscription, $body);
nlog('after response'); nlog("after response");
/* Append the response to the system logger body */ /* Append the response to the system logger body */
if(is_array($response)){ if(is_array($response)){
$body = $response; $body = $response;
} else {
}
else {
$body = $response->getStatusCode(); $body = $response->getStatusCode();
} }
$client = Client::where('id', $this->decodePrimaryKey($body['client']))->withTrashed()->first(); $client = Client::where('id', $this->decodePrimaryKey($body['client']))->withTrashed()->first();
@ -770,13 +822,13 @@ class SubscriptionService
$client->company, $client->company,
); );
nlog('ready to fire back'); nlog("ready to fire back");
if (is_array($body)) { if(is_array($body))
return $response; return $response;
} else { else
return ['message' => 'There was a problem encountered with the webhook', 'status_code' => 500]; return ['message' => 'There was a problem encountered with the webhook', 'status_code' => 500];
}
} }
public function fireNotifications() public function fireNotifications()
@ -792,18 +844,16 @@ class SubscriptionService
*/ */
public function products() public function products()
{ {
if (! $this->subscription->product_ids) { if(!$this->subscription->product_ids)
return collect(); return collect();
}
$keys = $this->transformKeys(explode(',', $this->subscription->product_ids)); $keys = $this->transformKeys(explode(",", $this->subscription->product_ids));
if (is_array($keys)) { if(is_array($keys))
return Product::whereIn('id', $keys)->get(); return Product::whereIn('id', $keys)->get();
} else { else
return Product::where('id', $keys)->get(); return Product::where('id', $keys)->get();
} }
}
/** /**
* Get the recurring products for the * Get the recurring products for the
@ -813,17 +863,18 @@ class SubscriptionService
*/ */
public function recurring_products() public function recurring_products()
{ {
if (! $this->subscription->recurring_product_ids) { if(!$this->subscription->recurring_product_ids)
return collect(); return collect();
}
$keys = $this->transformKeys(explode(',', $this->subscription->recurring_product_ids)); $keys = $this->transformKeys(explode(",", $this->subscription->recurring_product_ids));
if(is_array($keys)){ if(is_array($keys)){
return Product::whereIn('id', $keys)->get(); return Product::whereIn('id', $keys)->get();
} else { }
else{
return Product::where('id', $keys)->get(); return Product::where('id', $keys)->get();
} }
} }
/** /**
@ -844,6 +895,7 @@ class SubscriptionService
* Handle the cancellation of a subscription * Handle the cancellation of a subscription
* *
* @param RecurringInvoice $recurring_invoice * @param RecurringInvoice $recurring_invoice
*
*/ */
public function handleCancellation(RecurringInvoice $recurring_invoice) public function handleCancellation(RecurringInvoice $recurring_invoice)
{ {
@ -864,8 +916,11 @@ class SubscriptionService
$recurring_invoice_repo->archive($recurring_invoice); $recurring_invoice_repo->archive($recurring_invoice);
/* Refund only if we are in the window - and there is nothing outstanding on the invoice */ /* Refund only if we are in the window - and there is nothing outstanding on the invoice */
if ($refund_end_date->greaterThan(now()) && (int) $outstanding_invoice->balance == 0) { if($refund_end_date->greaterThan(now()) && (int)$outstanding_invoice->balance == 0)
if ($outstanding_invoice->payments()->exists()) { {
if($outstanding_invoice->payments()->exists())
{
$payment = $outstanding_invoice->payments()->first(); $payment = $outstanding_invoice->payments()->first();
$data = [ $data = [
@ -899,6 +954,7 @@ class SubscriptionService
$nmo->settings = $recurring_invoice->company->settings; $nmo->settings = $recurring_invoice->company->settings;
$recurring_invoice->company->company_users->each(function ($company_user) use ($nmo){ $recurring_invoice->company->company_users->each(function ($company_user) use ($nmo){
$methods = $this->findCompanyUserNotificationType($company_user, ['recurring_cancellation', 'all_notifications']); $methods = $this->findCompanyUserNotificationType($company_user, ['recurring_cancellation', 'all_notifications']);
//if mail is a method type -fire mail!! //if mail is a method type -fire mail!!
@ -907,14 +963,21 @@ class SubscriptionService
$nmo->to_user = $company_user->user; $nmo->to_user = $company_user->user;
NinjaMailerJob::dispatch($nmo); NinjaMailerJob::dispatch($nmo);
} }
}); });
return $this->handleRedirect('client/subscriptions'); return $this->handleRedirect('client/subscriptions');
} }
private function getDaysInFrequency() private function getDaysInFrequency()
{ {
switch ($this->subscription->frequency_id) { switch ($this->subscription->frequency_id) {
case RecurringInvoice::FREQUENCY_DAILY: case RecurringInvoice::FREQUENCY_DAILY:
return 1; return 1;
@ -943,6 +1006,7 @@ class SubscriptionService
default: default:
return 0; return 0;
} }
} }
public function getNextDateForFrequency($date, $frequency) public function getNextDateForFrequency($date, $frequency)
@ -977,6 +1041,7 @@ class SubscriptionService
} }
} }
/** /**
* 'email' => $this->email ?? $this->contact->email, * 'email' => $this->email ?? $this->contact->email,
* 'quantity' => $this->quantity, * 'quantity' => $this->quantity,
@ -984,15 +1049,16 @@ class SubscriptionService
*/ */
public function handleNoPaymentRequired(array $data) public function handleNoPaymentRequired(array $data)
{ {
$context = (new ZeroCostProduct($this->subscription, $data))->run(); $context = (new ZeroCostProduct($this->subscription, $data))->run();
// Forward payload to webhook // Forward payload to webhook
if (array_key_exists('context', $context)) { if(array_key_exists('context', $context))
$response = $this->triggerWebhook($context); $response = $this->triggerWebhook($context);
}
// Hit the redirect // Hit the redirect
return $this->handleRedirect($context['redirect_url']); return $this->handleRedirect($context['redirect_url']);
} }
/** /**
@ -1000,9 +1066,9 @@ class SubscriptionService
*/ */
private function handleRedirect($default_redirect) private function handleRedirect($default_redirect)
{ {
if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url']) >= 1) {
if(array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url']) >=1)
return redirect($this->subscription->webhook_configuration['return_url']); return redirect($this->subscription->webhook_configuration['return_url']);
}
return redirect($default_redirect); return redirect($default_redirect);
} }

View File

@ -87,6 +87,7 @@ class AccountTransformer extends EntityTransformer
'hosted_company_count' => (int) $account->hosted_company_count, 'hosted_company_count' => (int) $account->hosted_company_count,
'is_hosted' => (bool) Ninja::isHosted(), 'is_hosted' => (bool) Ninja::isHosted(),
'set_react_as_default_ap' => (bool) $account->set_react_as_default_ap, 'set_react_as_default_ap' => (bool) $account->set_react_as_default_ap,
'trial_days_left' => Ninja::isHosted() ? (int) $account->getTrialDays() : 0,
]; ];
} }
@ -110,6 +111,5 @@ class AccountTransformer extends EntityTransformer
return $this->includeItem(auth()->user(), $transformer, User::class); return $this->includeItem(auth()->user(), $transformer, User::class);
// return $this->includeItem($account->default_company->owner(), $transformer, User::class);
} }
} }

View File

@ -78,7 +78,6 @@ class ExpenseTransformer extends EntityTransformer
'transaction_reference' => (string) $expense->transaction_reference ?: '', 'transaction_reference' => (string) $expense->transaction_reference ?: '',
'transaction_id' => (string) $expense->transaction_id ?: '', 'transaction_id' => (string) $expense->transaction_id ?: '',
'date' => $expense->date ?: '', 'date' => $expense->date ?: '',
//'expense_date' => $expense->date ?: '',
'number' => (string)$expense->number ?: '', 'number' => (string)$expense->number ?: '',
'payment_date' => $expense->payment_date ?: '', 'payment_date' => $expense->payment_date ?: '',
'custom_value1' => $expense->custom_value1 ?: '', 'custom_value1' => $expense->custom_value1 ?: '',

View File

@ -11,8 +11,10 @@
namespace App\Transformers; namespace App\Transformers;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation; use App\Models\PurchaseOrderInvitation;
use App\Transformers\DocumentTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
class PurchaseOrderTransformer extends EntityTransformer class PurchaseOrderTransformer extends EntityTransformer
@ -21,6 +23,11 @@ class PurchaseOrderTransformer extends EntityTransformer
protected $defaultIncludes = [ protected $defaultIncludes = [
'invitations', 'invitations',
'documents'
];
protected $availableIncludes = [
'expense'
]; ];
public function includeInvitations(PurchaseOrder $purchase_order) public function includeInvitations(PurchaseOrder $purchase_order)
@ -30,6 +37,21 @@ class PurchaseOrderTransformer extends EntityTransformer
return $this->includeCollection($purchase_order->invitations, $transformer, PurchaseOrderInvitation::class); return $this->includeCollection($purchase_order->invitations, $transformer, PurchaseOrderInvitation::class);
} }
public function includeDocuments(PurchaseOrder $purchase_order)
{
$transformer = new DocumentTransformer($this->serializer);
return $this->includeCollection($purchase_order->documents, $transformer, Document::class);
}
public function includeExpense(PurchaseOrder $purchase_order)
{
$transformer = new ExpenseTransformer($this->serializer);
return $this->includeItem($purchase_order->expense, $transformer, Document::class);
}
public function transform(PurchaseOrder $purchase_order) public function transform(PurchaseOrder $purchase_order)
{ {
return [ return [
@ -88,10 +110,12 @@ class PurchaseOrderTransformer extends EntityTransformer
'custom_surcharge_tax3' => (bool)$purchase_order->custom_surcharge_tax3, 'custom_surcharge_tax3' => (bool)$purchase_order->custom_surcharge_tax3,
'custom_surcharge_tax4' => (bool)$purchase_order->custom_surcharge_tax4, 'custom_surcharge_tax4' => (bool)$purchase_order->custom_surcharge_tax4,
'line_items' => $purchase_order->line_items ?: (array)[], 'line_items' => $purchase_order->line_items ?: (array)[],
'entity_type' => 'purchase_order', 'entity_type' => 'purchaseOrder',
'exchange_rate' => (float)$purchase_order->exchange_rate, 'exchange_rate' => (float)$purchase_order->exchange_rate,
'paid_to_date' => (float)$purchase_order->paid_to_date, 'paid_to_date' => (float)$purchase_order->paid_to_date,
'subscription_id' => $this->encodePrimaryKey($purchase_order->subscription_id), 'subscription_id' => $this->encodePrimaryKey($purchase_order->subscription_id),
'expense_id' => $this->encodePrimaryKey($purchase_order->expense_id),
]; ];
} }
} }

View File

@ -13,7 +13,10 @@ namespace App\Transformers;
use App\Models\Document; use App\Models\Document;
use App\Models\Task; use App\Models\Task;
use App\Models\TaskStatus;
use App\Transformers\TaskStatusTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use League\Fractal\Resource\Item;
/** /**
* class TaskTransformer. * class TaskTransformer.
@ -30,6 +33,8 @@ class TaskTransformer extends EntityTransformer
* @var array * @var array
*/ */
protected $availableIncludes = [ protected $availableIncludes = [
'client',
'status'
]; ];
public function includeDocuments(Task $task) public function includeDocuments(Task $task)
@ -39,6 +44,27 @@ class TaskTransformer extends EntityTransformer
return $this->includeCollection($task->documents, $transformer, Document::class); return $this->includeCollection($task->documents, $transformer, Document::class);
} }
public function includeClient(Task $task): ?Item
{
$transformer = new ClientTransformer($this->serializer);
if(!$task->client)
return null;
return $this->includeItem($task->client, $transformer, Client::class);
}
public function includeStatus(Task $task): ?Item
{
$transformer = new TaskStatusTransformer($this->serializer);
if(!$task->status)
return null;
return $this->includeItem($task->status, $transformer, TaskStatus::class);
}
public function transform(Task $task) public function transform(Task $task)
{ {
return [ return [

View File

@ -12,6 +12,7 @@
namespace App\Utils; namespace App\Utils;
use App\Helpers\SwissQr\SwissQrGenerator;
use App\Models\Country; use App\Models\Country;
use App\Models\CreditInvitation; use App\Models\CreditInvitation;
use App\Models\GatewayType; use App\Models\GatewayType;
@ -160,6 +161,17 @@ class HtmlEngine
if ($this->entity->vendor) { if ($this->entity->vendor) {
$data['$invoice.vendor'] = ['value' => $this->entity->vendor->present()->name(), 'label' => ctrans('texts.vendor_name')]; $data['$invoice.vendor'] = ['value' => $this->entity->vendor->present()->name(), 'label' => ctrans('texts.vendor_name')];
} }
if(strlen($this->company->getSetting('qr_iban')) > 5 && strlen($this->company->getSetting('besr_id')) > 1)
{
try{
$data['$swiss_qr'] = ['value' => (new SwissQrGenerator($this->entity, $this->company))->run(), 'label' => ''];
}
catch(\Exception $e){
$data['$swiss_qr'] = ['value' => '', 'label' => ''];
}
}
} }
if ($this->entity_string == 'quote') { if ($this->entity_string == 'quote') {
@ -275,6 +287,7 @@ class HtmlEngine
$data['$assigned_to_user'] = ['value' => $this->entity->assigned_user ? $this->entity->assigned_user->present()->name() : '', 'label' => ctrans('texts.name')]; $data['$assigned_to_user'] = ['value' => $this->entity->assigned_user ? $this->entity->assigned_user->present()->name() : '', 'label' => ctrans('texts.name')];
$data['$user_iban'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')]; $data['$user_iban'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
$data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')]; $data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
$data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')]; $data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; $data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];

View File

@ -16,16 +16,22 @@ use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\Quote; use App\Models\Quote;
use App\Models\QuoteInvitation; use App\Models\QuoteInvitation;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml; use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\MakesTemplateData; use App\Utils\Traits\MakesTemplateData;
use App\Utils\VendorHtmlEngine;
use DB; use DB;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Lang; use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Str;
use League\CommonMark\CommonMarkConverter; use League\CommonMark\CommonMarkConverter;
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles; use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;
@ -79,6 +85,7 @@ class TemplateEngine
public function build() public function build()
{ {
return $this->setEntity() return $this->setEntity()
->setSettingsObject() ->setSettingsObject()
->setTemplates() ->setTemplates()
@ -89,7 +96,7 @@ class TemplateEngine
private function setEntity() private function setEntity()
{ {
if (strlen($this->entity) > 1 && strlen($this->entity_id) > 1) { if (strlen($this->entity) > 1 && strlen($this->entity_id) > 1) {
$class = 'App\Models\\'.ucfirst($this->entity); $class = 'App\Models\\'.ucfirst(Str::camel($this->entity));
$this->entity_obj = $class::withTrashed()->where('id', $this->decodePrimaryKey($this->entity_id))->company()->first(); $this->entity_obj = $class::withTrashed()->where('id', $this->decodePrimaryKey($this->entity_id))->company()->first();
} else { } else {
$this->mockEntity(); $this->mockEntity();
@ -100,7 +107,11 @@ class TemplateEngine
private function setSettingsObject() private function setSettingsObject()
{ {
if ($this->entity_obj) { if($this->entity == 'purchaseOrder'){
$this->settings_entity = auth()->user()->company();
$this->settings = $this->settings_entity->settings;
}
elseif ($this->entity_obj->client()->exists()) {
$this->settings_entity = $this->entity_obj->client; $this->settings_entity = $this->entity_obj->client;
$this->settings = $this->settings_entity->getMergedSettings(); $this->settings = $this->settings_entity->getMergedSettings();
} else { } else {
@ -144,9 +155,13 @@ class TemplateEngine
$this->raw_body = $this->body; $this->raw_body = $this->body;
$this->raw_subject = $this->subject; $this->raw_subject = $this->subject;
if ($this->entity_obj) { if($this->entity == 'purchaseOrder'){
$this->fakerValues();
}
elseif ($this->entity_obj->client()->exists()) {
$this->entityValues($this->entity_obj->client->primary_contact()->first()); $this->entityValues($this->entity_obj->client->primary_contact()->first());
} else { }
else {
$this->fakerValues(); $this->fakerValues();
} }
@ -168,16 +183,19 @@ class TemplateEngine
'allow_unsafe_links' => false, 'allow_unsafe_links' => false,
]); ]);
$this->body = $converter->convert($this->body); $this->body = $converter->convert($this->body)->getContent();
} }
private function entityValues($contact) private function entityValues($contact)
{ {
if($this->entity == 'purchaseOrder')
$this->labels_and_values = (new VendorHtmlEngine($this->entity_obj->invitations->first()))->generateLabelsAndValues();
else
$this->labels_and_values = (new HtmlEngine($this->entity_obj->invitations->first()))->generateLabelsAndValues(); $this->labels_and_values = (new HtmlEngine($this->entity_obj->invitations->first()))->generateLabelsAndValues();
$this->body = strtr($this->body, $this->labels_and_values['labels']); $this->body = strtr($this->body, $this->labels_and_values['labels']);
$this->body = strtr($this->body, $this->labels_and_values['values']); $this->body = strtr($this->body, $this->labels_and_values['values']);
// $this->body = str_replace("\n", "<br>", $this->body);
$this->subject = strtr($this->subject, $this->labels_and_values['labels']); $this->subject = strtr($this->subject, $this->labels_and_values['labels']);
$this->subject = strtr($this->subject, $this->labels_and_values['values']); $this->subject = strtr($this->subject, $this->labels_and_values['values']);
@ -199,7 +217,18 @@ class TemplateEngine
$data['footer'] = ''; $data['footer'] = '';
$data['logo'] = auth()->user()->company()->present()->logo(); $data['logo'] = auth()->user()->company()->present()->logo();
if($this->entity_obj->client()->exists())
$data = array_merge($data, Helpers::sharedEmailVariables($this->entity_obj->client)); $data = array_merge($data, Helpers::sharedEmailVariables($this->entity_obj->client));
else{
$data['signature'] = $this->settings->email_signature;
$data['settings'] = $this->settings;
$data['whitelabel'] = $this->entity_obj ? $this->entity_obj->company->account->isPaid() : true;
$data['company'] = $this->entity_obj ? $this->entity_obj->company : '';
$data['settings'] = $this->settings;
}
if ($email_style == 'custom') { if ($email_style == 'custom') {
$wrapper = $this->settings_entity->getSetting('email_style_custom'); $wrapper = $this->settings_entity->getSetting('email_style_custom');
@ -240,8 +269,13 @@ class TemplateEngine
private function mockEntity() private function mockEntity()
{ {
if(!$this->entity && $this->template && str_contains($this->template, 'purchase_order'))
$this->entity = 'purchaseOrder';
DB::connection(config('database.default'))->beginTransaction(); DB::connection(config('database.default'))->beginTransaction();
$vendor = false;
$client = Client::factory()->create([ $client = Client::factory()->create([
'user_id' => auth()->user()->id, 'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id, 'company_id' => auth()->user()->company()->id,
@ -285,6 +319,53 @@ class TemplateEngine
]); ]);
} }
if($this->entity == 'purchaseOrder')
{
$vendor = Vendor::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
]);
$contact = VendorContact::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'vendor_id' => $vendor->id,
'is_primary' => 1,
'send_email' => true,
]);
$this->entity_obj = PurchaseOrder::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'vendor_id' => $vendor->id,
]);
$invitation = PurchaseOrderInvitation::factory()->create([
'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id,
'purchase_order_id' => $this->entity_obj->id,
'vendor_contact_id' => $contact->id,
]);
}
if($vendor)
{
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('vendor', $vendor);
$this->entity_obj->setRelation('company', auth()->user()->company());
$this->entity_obj->load('vendor');
$vendor->setRelation('company', auth()->user()->company());
$vendor->load('company');
}
else
{
$this->entity_obj->setRelation('invitations', $invitation); $this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('client', $client); $this->entity_obj->setRelation('client', $client);
$this->entity_obj->setRelation('company', auth()->user()->company()); $this->entity_obj->setRelation('company', auth()->user()->company());
@ -292,6 +373,7 @@ class TemplateEngine
$client->setRelation('company', auth()->user()->company()); $client->setRelation('company', auth()->user()->company());
$client->load('company'); $client->load('company');
} }
}
private function tearDown() private function tearDown()
{ {

View File

@ -109,6 +109,10 @@ trait AppSetup
'subject' => EmailTemplateDefaults::emailCreditSubject(), 'subject' => EmailTemplateDefaults::emailCreditSubject(),
'body' => EmailTemplateDefaults::emailCreditTemplate(), 'body' => EmailTemplateDefaults::emailCreditTemplate(),
], ],
'purchase_order' => [
'subject' => EmailTemplateDefaults::emailPurchaseOrderSubject(),
'body' => EmailTemplateDefaults::emailPurchaseOrderTemplate(),
],
]; ];
Cache::forever($name, $data); Cache::forever($name, $data);

View File

@ -114,6 +114,9 @@ trait CompanySettingsSaver
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') { elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') {
$value = 'integer'; $value = 'integer';
if($key == 'besr_id')
$value = 'string';
if (! property_exists($settings, $key)) { if (! property_exists($settings, $key)) {
continue; continue;
} elseif (! $this->checkAttribute($value, $settings->{$key})) { } elseif (! $this->checkAttribute($value, $settings->{$key})) {
@ -182,6 +185,9 @@ trait CompanySettingsSaver
$value = 'string'; $value = 'string';
} }
if($key == 'besr_id')
$value = 'string';
if (! property_exists($settings, $key)) { if (! property_exists($settings, $key)) {
continue; continue;
} elseif ($this->checkAttribute($value, $settings->{$key})) { } elseif ($this->checkAttribute($value, $settings->{$key})) {

View File

@ -200,56 +200,33 @@ trait MakesTemplateData
$data['$task.tax_name3'] = ['value' => 'CA Sales Tax', 'label' => ctrans('texts.tax')]; $data['$task.tax_name3'] = ['value' => 'CA Sales Tax', 'label' => ctrans('texts.tax')];
$data['$task.line_total'] = ['value' => '$100.00', 'label' => ctrans('texts.line_total')]; $data['$task.line_total'] = ['value' => '$100.00', 'label' => ctrans('texts.line_total')];
//$data['$paid_to_date'] = ; $data['$vendor_name'] = ['value' => 'Joey Diaz Denkins', 'label' => ctrans('texts.vendor_name')];;
// $data['$your_invoice'] = ; $data['$vendor.name'] = &$data['$vendor_name'];
// $data['$quote'] = ; $data['$vendor'] = &$data['$vendor_name'];
// $data['$your_quote'] = ;
// $data['$invoice_issued_to'] = ;
// $data['$quote_issued_to'] = ;
// $data['$rate'] = ;
// $data['$hours'] = ;
// $data['$from'] = ;
// $data['$to'] = ;
// $data['$invoice_to'] = ;
// $data['$quote_to'] = ;
// $data['$details'] = ;
// $data['custom_label1'] = ['value' => '', 'label' => ctrans('texts.')];
// $data['custom_label2'] = ['value' => '', 'label' => ctrans('texts.')];
// $data['custom_label3'] = ['value' => '', 'label' => ctrans('texts.')];
// $data['custom_label4'] = ['value' => '', 'label' => ctrans('texts.')];
//$data['$blank'] = ;
//$data['$surcharge'] = ;
/*
$data['$tax_invoice'] =
$data['$tax_quote'] =
$data['$statement'] = ;
$data['$statement_date'] = ;
$data['$your_statement'] = ;
$data['$statement_issued_to'] = ;
$data['$statement_to'] = ;
$data['$credit_note'] = ;
$data['$credit_date'] = ;
$data['$credit_issued_to'] = ;
$data['$credit_to'] = ;
$data['$your_credit'] = ;
$data['$phone'] = ;
$data['$outstanding'] = ; $data['$vendor.address1'] = &$data['$address1'];
$data['$invoice_due_date'] = ; $data['$vendor.address2'] = &$data['$address2'];
$data['$quote_due_date'] = ; $data['$vendor_address'] = ['value' => '5 Kalamazoo Way\n Jimbuckeroo\n USA 90210', 'label' => ctrans('texts.address')];
$data['$service'] = ; $data['$vendor.address'] = &$data['$vendor_address'];
$data['$product_key'] = ; $data['$vendor.postal_code'] = ['value' => '90210', 'label' => ctrans('texts.postal_code')];
$data['$unit_cost'] = ; $data['$vendor.public_notes'] = $data['$invoice.public_notes'];
$data['$custom_value1'] = ; $data['$vendor.city'] = &$data['$company.city'];
$data['$custom_value2'] = ; $data['$vendor.state'] = &$data['$company.state'];
$data['$delivery_note'] = ; $data['$vendor.id_number'] = &$data['$id_number'];
$data['$date'] = ; $data['$vendor.vat_number'] = &$data['$vat_number'];
$data['$method'] = ; $data['$vendor.website'] = &$data['$website'];
$data['$payment_date'] = ; $data['$vendor.phone'] = &$data['$phone'];
$data['$reference'] = ; $data['$vendor.city_state_postal'] = &$data['$city_state_postal'];
$data['$amount'] = ; $data['$vendor.postal_city_state'] = &$data['$postal_city_state'];
$data['$amount_paid'] =; $data['$vendor.country'] = &$data['$country'];
*/ $data['$vendor.email'] = &$data['$email'];
$data['$vendor.billing_address1'] = &$data['$vendor.address1'];
$data['$vendor.billing_address2'] = &$data['$vendor.address2'];
$data['$vendor.billing_city'] = &$data['$vendor.city'];
$data['$vendor.billing_state'] = &$data['$vendor.state'];
$data['$vendor.billing_postal_code'] = &$data['$vendor.postal_code'];
$data['$vendor.billing_country'] = &$data['$vendor.country'];
$arrKeysLength = array_map('strlen', array_keys($data)); $arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data); array_multisort($arrKeysLength, SORT_DESC, $data);

View File

@ -55,9 +55,8 @@ trait SettingsSaver
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && strlen($settings->{$key}) >= 1)) { elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && strlen($settings->{$key}) >= 1)) {
$value = 'integer'; $value = 'integer';
if ($key == 'gmail_sending_user_id') { if($key == 'gmail_sending_user_id' || $key == 'besr_id')
$value = 'string'; $value = 'string';
}
if (! property_exists($settings, $key)) { if (! property_exists($settings, $key)) {
continue; continue;

View File

@ -63,6 +63,11 @@ class VendorHtmlEngine
$this->vendor = $this->contact->vendor->load('company','country'); $this->vendor = $this->contact->vendor->load('company','country');
if(!$this->vendor->currency_id){
$this->vendor->currency_id = $this->company->settings->currency_id;
$this->vendor->save();
}
$this->entity->load('vendor'); $this->entity->load('vendor');
$this->settings = $this->company->settings; $this->settings = $this->company->settings;
@ -72,28 +77,29 @@ class VendorHtmlEngine
$this->helpers = new Helpers(); $this->helpers = new Helpers();
} }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function resolveEntityString() private function resolveEntityString()
{ {
switch ($this->invitation) { switch ($this->invitation) {
case $this->invitation instanceof InvoiceInvitation: case ($this->invitation instanceof InvoiceInvitation):
return 'invoice'; return 'invoice';
break; break;
case $this->invitation instanceof CreditInvitation: case ($this->invitation instanceof CreditInvitation):
return 'credit'; return 'credit';
break; break;
case $this->invitation instanceof QuoteInvitation: case ($this->invitation instanceof QuoteInvitation):
return 'quote'; return 'quote';
break; break;
case $this->invitation instanceof RecurringInvoiceInvitation: case ($this->invitation instanceof RecurringInvoiceInvitation):
return 'recurring_invoice'; return 'recurring_invoice';
break; break;
case $this->invitation instanceof PurchaseOrderInvitation: case ($this->invitation instanceof PurchaseOrderInvitation):
return 'purchase_order'; return 'purchase_order';
break; break;
default: default:
// code... # code...
break; break;
} }
} }
@ -157,23 +163,25 @@ class VendorHtmlEngine
$data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')]; $data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')];
$data['$gross_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getGrossSubTotal(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')]; $data['$gross_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getGrossSubTotal(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')];
if ($this->entity->uses_inclusive_taxes) { if($this->entity->uses_inclusive_taxes)
$data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes - $this->entity_calc->getTotalDiscount()), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')]; $data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes - $this->entity_calc->getTotalDiscount()), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
} else { else
$data['$net_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal() - $this->entity_calc->getTotalDiscount(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')]; $data['$net_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal() - $this->entity_calc->getTotalDiscount(), $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
}
if ($this->entity->partial > 0) { if ($this->entity->partial > 0) {
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')]; $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
$data['$amount_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')]; $data['$amount_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; $data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
} else { } else {
if($this->entity->status_id == 1){ if($this->entity->status_id == 1){
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->amount, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')]; $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->amount, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.balance_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.balance_due')];
$data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')];
} else { }
else{
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')]; $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->vendor) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
$data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')];
@ -201,7 +209,7 @@ class VendorHtmlEngine
$data['$created_by_user'] = &$data['$user.name']; $data['$created_by_user'] = &$data['$user.name'];
$data['$assigned_to_user'] = ['value' => $this->entity->assigned_user ? $this->entity->assigned_user->present()->name() : '', 'label' => ctrans('texts.name')]; $data['$assigned_to_user'] = ['value' => $this->entity->assigned_user ? $this->entity->assigned_user->present()->name() : '', 'label' => ctrans('texts.name')];
$data['$public_notes'] = ['value' => $this->entity->public_notes, 'label' => ctrans('texts.public_notes')]; $data['$public_notes'] = ['value' => $this->entity->public_notes, 'label' => ctrans("texts.public_notes")];
$data['$entity.public_notes'] = &$data['$public_notes']; $data['$entity.public_notes'] = &$data['$public_notes'];
$data['$notes'] = &$data['$public_notes']; $data['$notes'] = &$data['$public_notes'];
@ -229,9 +237,8 @@ class VendorHtmlEngine
$data['$country_2'] = ['value' => isset($this->vendor->country) ? $this->vendor->country->iso_3166_2 : '', 'label' => ctrans('texts.country')]; $data['$country_2'] = ['value' => isset($this->vendor->country) ? $this->vendor->country->iso_3166_2 : '', 'label' => ctrans('texts.country')];
$data['$email'] = ['value' => isset($this->contact) ? $this->contact->email : 'no contact email on record', 'label' => ctrans('texts.email')]; $data['$email'] = ['value' => isset($this->contact) ? $this->contact->email : 'no contact email on record', 'label' => ctrans('texts.email')];
if (str_contains($data['$email']['value'], 'example.com')) { if(str_contains($data['$email']['value'], 'example.com'))
$data['$email'] = ['value' => '', 'label' => ctrans('texts.email')]; $data['$email'] = ['value' => '', 'label' => ctrans('texts.email')];
}
$data['$vendor_name'] = ['value' => $this->vendor->present()->name() ?: '&nbsp;', 'label' => ctrans('texts.vendor_name')]; $data['$vendor_name'] = ['value' => $this->vendor->present()->name() ?: '&nbsp;', 'label' => ctrans('texts.vendor_name')];
$data['$vendor.name'] = &$data['$vendor_name']; $data['$vendor.name'] = &$data['$vendor_name'];
@ -383,8 +390,8 @@ class VendorHtmlEngine
$data['$autoBill'] = ['value' => ctrans('texts.auto_bill_notification_placeholder'), 'label' => '']; $data['$autoBill'] = ['value' => ctrans('texts.auto_bill_notification_placeholder'), 'label' => ''];
$data['$auto_bill'] = &$data['$autoBill']; $data['$auto_bill'] = &$data['$autoBill'];
$data['$dir'] = ['value' => $this->company->language()?->locale === 'ar' ? 'rtl' : 'ltr', 'label' => '']; $data['$dir'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];
$data['$dir_text_align'] = ['value' => $this->company->language()?->locale === 'ar' ? 'right' : 'left', 'label' => '']; $data['$dir_text_align'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'right' : 'left', 'label' => ''];
$data['$payment.date'] = ['value' => '&nbsp;', 'label' => ctrans('texts.payment_date')]; $data['$payment.date'] = ['value' => '&nbsp;', 'label' => ctrans('texts.payment_date')];
$data['$method'] = ['value' => '&nbsp;', 'label' => ctrans('texts.method')]; $data['$method'] = ['value' => '&nbsp;', 'label' => ctrans('texts.method')];
@ -483,20 +490,19 @@ class VendorHtmlEngine
return '&nbsp;'; return '&nbsp;';
} }
private function getCountryCode() :string private function getCountryCode() :string
{ {
$country = Country::find($this->settings->country_id); $country = Country::find($this->settings->country_id);
if ($country) { if($country)
return $country->iso_3166_2; return $country->iso_3166_2;
}
// if ($country) { // if ($country) {
// return ctrans('texts.country_' . $country->iso_3166_2); // return ctrans('texts.country_' . $country->iso_3166_2);
// } // }
return '&nbsp;'; return '&nbsp;';
} }
/** /**
* Due to the way we are compiling the blade template we * Due to the way we are compiling the blade template we
* have no ability to iterate, so in the case * have no ability to iterate, so in the case

View File

@ -81,6 +81,7 @@
"setasign/fpdi": "^2.3", "setasign/fpdi": "^2.3",
"socialiteproviders/apple": "^5.2", "socialiteproviders/apple": "^5.2",
"socialiteproviders/microsoft": "^4.1", "socialiteproviders/microsoft": "^4.1",
"sprain/swiss-qr-bill": "^3.2",
"square/square": "13.0.0.20210721", "square/square": "13.0.0.20210721",
"stripe/stripe-php": "^7.50", "stripe/stripe-php": "^7.50",
"symfony/http-client": "^6.0", "symfony/http-client": "^6.0",

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.4.4', 'app_version' => '5.4.10',
'app_tag' => '5.4.4', 'app_tag' => '5.4.10',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),
@ -194,4 +194,7 @@ return [
'ninja_apple_bundle_id' => env('APPLE_BUNDLE_ID', false), 'ninja_apple_bundle_id' => env('APPLE_BUNDLE_ID', false),
'ninja_apple_issuer_id' => env('APPLE_ISSUER_ID', false), 'ninja_apple_issuer_id' => env('APPLE_ISSUER_ID', false),
'react_app_enabled' => env('REACT_APP_ENABLED', false), 'react_app_enabled' => env('REACT_APP_ENABLED', false),
'ninja_apple_client_id' => env('APPLE_CLIENT_ID', false),
'ninja_apple_client_secret' => env('APPLE_CLIENT_SECRET',false),
'ninja_apple_redirect_url' => env('APPLE_REDIRECT_URI',false),
]; ];

View File

@ -0,0 +1,51 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Database\Factories;
use App\Factory\InvoiceItemFactory;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use Illuminate\Database\Eloquent\Factories\Factory;
class PurchaseOrderFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = PurchaseOrder::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'status_id' => Invoice::STATUS_SENT,
'number' => $this->faker->ean13(),
'discount' => $this->faker->numberBetween(1, 10),
'is_amount_discount' => (bool) random_int(0, 1),
'tax_name1' => 'GST',
'tax_rate1' => 10,
'tax_name2' => 'VAT',
'tax_rate2' => 17.5,
'is_deleted' => false,
'po_number' => $this->faker->text(10),
'date' => $this->faker->date(),
'due_date' => $this->faker->date(),
'line_items' => InvoiceItemFactory::generate(5),
'terms' => $this->faker->text(500),
];
}
}

View File

@ -0,0 +1,50 @@
<?php
use App\Models\Gateway;
use App\Models\GatewayType;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class FortePaymentGateway extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$fields = new \stdClass;
$fields->testMode = false;
$fields->apiLoginId = "";
$fields->apiAccessId = "";
$fields->secureKey = "";
$fields->authOrganizationId = "";
$fields->organizationId = "";
$fields->locationId = "";
$forte = new Gateway;
$forte->id = 59;
$forte->name = 'Forte';
$forte->key = 'kivcvjexxvdiyqtj3mju5d6yhpeht2xs';
$forte->provider = 'Forte';
$forte->is_offsite = true;
$forte->fields = \json_encode($fields);
$forte->visible = 1;
$forte->site_url = 'https://www.forte.net/';
$forte->default_gateway_type_id = GatewayType::CREDIT_CARD;
$forte->save();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

Some files were not shown because too many files have changed in this diff Show More