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*/
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 $enable_client_portal_tasks = false; //@ben to implement
@ -433,7 +436,12 @@ class CompanySettings extends BaseSettings
public $auto_archive_invoice_cancelled = false;
public $vendor_portal_enable_uploads=false;
public static $casts = [
'vendor_portal_enable_uploads' => 'bool',
'besr_id' => 'string',
'qr_iban' => 'string',
'email_subject_purchase_order' => 'string',
'email_template_purchase_order' => 'string',
'require_purchase_order_signature' => 'bool',

View File

@ -13,7 +13,8 @@ class PaymentFailed extends Exception
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', [
'message' => $this->getMessage(),
'code' => $this->getCode(),

View File

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

View File

@ -68,7 +68,7 @@ class InvoiceFilters extends QueryFilters
return $this->builder;
}
public function number(string $number) :Builder
public function number(string $number = '') :Builder
{
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()
{
return $this->builder

View File

@ -94,6 +94,9 @@ class QuoteFilters extends QueryFilters
{
$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]);
}

View File

@ -102,16 +102,12 @@ class InvoiceSum
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_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_tax2);
$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_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_tax4);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4);
$this->total += $this->total_custom_values;
@ -155,7 +151,7 @@ class InvoiceSum
*/
private function calculateBalance()
{
//$this->invoice->balance = $this->balance($this->getTotal(), $this->invoice);
$this->setCalculatedAttributes();
return $this;
@ -174,22 +170,6 @@ class InvoiceSum
{
$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;
}

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

View File

@ -80,6 +80,7 @@ class BaseController extends Controller
'company.groups.documents',
'company.invoices.invitations.contact',
'company.invoices.invitations.company',
'company.purchase_orders.invitations',
'company.invoices.documents',
'company.products',
'company.products.documents',
@ -767,6 +768,10 @@ class BaseController extends Controller
return redirect('/')->with(['login' => 'true']);
}
if (request()->has('signup') && request()->input('signup') == 'true') {
return redirect('/')->with(['signup' => 'true']);
}
$data = [];
//pass report errors bool to front end
@ -776,11 +781,16 @@ class BaseController extends Controller
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
$data['build'] = request()->has('build') ? request()->input('build') : '';
$data['login'] = request()->has('login') ? request()->input('login') : 'false';
$data['signup'] = request()->has('signup') ? request()->input('signup') : 'false';
if (request()->session()->has('login')) {
$data['login'] = 'true';
}
if(request()->session()->has('signup')){
$data['signup'] = 'true';
}
$data['user_agent'] = request()->server('HTTP_USER_AGENT');
$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
}
/**
* 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')
->firstOrFail();
if($invitation->contact->trashed())
$invitation->contact->restore();
auth()->guard('contact')->loginUsingId($invitation->contact->id, true);
$invoice = $invitation->invoice;

View File

@ -25,6 +25,7 @@ use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Models\Subscription;
use App\Notifications\Ninja\NewAccountNotification;
use App\Repositories\SubscriptionRepository;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
@ -156,6 +157,9 @@ class NinjaPlanController extends Controller
->increment()
->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);
}

View File

@ -90,13 +90,17 @@ class PaymentController extends Controller
public function response(PaymentResponseRequest $request)
{
$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);
$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
// ->driver(auth()->user()->client)
->driver($client)
->setPaymentMethod($request->input('payment_method_id'))
->setPaymentHash($payment_hash)

View File

@ -180,7 +180,7 @@ class QuoteController extends Controller
if ($process) {
foreach ($quotes as $quote) {
$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)) {
InjectSignature::dispatch($quote, request()->signature);

View File

@ -22,6 +22,7 @@ use Google_Client;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Microsoft\Graph\Model;
class ConnectedAccountController extends BaseController
{
@ -81,12 +82,61 @@ class ConnectedAccountController extends BaseController
return $this->handleGoogleOauth();
}
if ($request->input('provider') == 'microsoft') {
return $this->handleMicrosoftOauth($request);
}
return response()
->json(['message' => 'Provider not supported'], 400)
->header('X-App-Version', config('ninja.app_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()
{
$user = false;

View File

@ -602,7 +602,18 @@ class CreditController extends BaseController
}
break;
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) {
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\Jobs\Entity\EmailEntity;
use App\Jobs\Mail\EntitySentMailer;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Transformers\CreditTransformer;
use App\Transformers\InvoiceTransformer;
use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\QuoteTransformer;
use App\Transformers\RecurringInvoiceTransformer;
use App\Utils\Ninja;
@ -125,6 +128,10 @@ class EmailController extends BaseController
'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) {
if (! $invitation->contact->trashed() && $invitation->contact->email) {
$entity_obj->service()->markSent()->save();
@ -172,4 +179,17 @@ class EmailController extends BaseController
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);
}
break;
// case 'reverse':
// $invoice = $invoice->service()->handleReversal()->deletePdf()->save();
// if (! $bulk) {
// $this->itemResponse($invoice);
// }
// break;
case 'email':
//check query parameter for email_type and set the template else use calculateTemplate
@ -767,6 +761,24 @@ class InvoiceController extends BaseController
}
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:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
break;

View File

@ -272,7 +272,10 @@ class PreviewController extends BaseController
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
} catch (\Exception $e) {
}
catch(\Exception $e){
nlog($e->getMessage());
DB::connection(config('database.default'))->rollBack();
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());
if ($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;
use App\Events\PurchaseOrder\PurchaseOrderWasCreated;
use App\Events\PurchaseOrder\PurchaseOrderWasUpdated;
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\StorePurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\UpdatePurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\UploadPurchaseOrderRequest;
use App\Jobs\Invoice\ZipInvoices;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Jobs\PurchaseOrder\ZipPurchaseOrders;
use App\Models\Account;
use App\Models\Client;
use App\Models\Expense;
use App\Models\PurchaseOrder;
use App\Repositories\PurchaseOrderRepository;
use App\Transformers\ExpenseTransformer;
use App\Transformers\PurchaseOrderTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
class PurchaseOrderController extends BaseController
{
use MakesHash;
use SavesDocuments;
protected $entity_type = PurchaseOrder::class;
protected $entity_transformer = PurchaseOrderTransformer::class;
protected $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;
}
/**
* Show the list of Purchase Orders.
*
@ -97,7 +101,6 @@ class PurchaseOrderController extends BaseController
return $this->listResponse($purchase_orders);
}
/**
* Show the form for creating a new resource.
*
@ -143,7 +146,6 @@ class PurchaseOrderController extends BaseController
return $this->itemResponse($purchase_order);
}
/**
* Store a newly created resource in storage.
*
@ -185,6 +187,7 @@ class PurchaseOrderController extends BaseController
*/
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 = $purchase_order->service()
@ -196,7 +199,6 @@ class PurchaseOrderController extends BaseController
return $this->itemResponse($purchase_order);
}
/**
* Display the specified resource.
*
@ -252,7 +254,6 @@ class PurchaseOrderController extends BaseController
{
return $this->itemResponse($purchase_order);
}
/**
* Show the form for editing the specified resource.
*
@ -307,7 +308,6 @@ class PurchaseOrderController extends BaseController
{
return $this->itemResponse($purchase_order);
}
/**
* 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 = $purchase_order->service()
->triggeredActions($request)
->save();
event(new PurchaseOrderWasUpdated($purchase_order, $purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
return $this->itemResponse($purchase_order);
}
/**
* Remove the specified resource from storage.
*
@ -481,6 +484,7 @@ class PurchaseOrderController extends BaseController
*/
public function bulk()
{
$action = request()->input('action');
$ids = request()->input('ids');
@ -498,8 +502,7 @@ class PurchaseOrderController extends BaseController
if ($action == 'bulk_download' && $purchase_orders->count() >= 1) {
$purchase_orders->each(function ($purchase_order) {
if (auth()->user()->cannot('view', $purchase_order)) {
nlog('access denied');
nlog("access denied");
return response()->json(['message' => ctrans('text.access_denied')]);
}
});
@ -643,10 +646,113 @@ class PurchaseOrderController extends BaseController
if (! $bulk) {
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:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
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);
break;
case 'send_email':
$quote->service()->sendEmail();
return response()->json(['message'=> ctrans('texts.sent_message')], 200);
break;
case 'mark_sent':
$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));
// $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()
->triggeredActions($request)
->save();
@ -700,6 +696,15 @@ class RecurringInvoiceController extends BaseController
$this->itemResponse($recurring_invoice);
}
break;
case 'send_now':
$recurring_invoice = $recurring_invoice->service()->sendNow();
if (! $bulk) {
$this->itemResponse($recurring_invoice);
}
break;
default:
// code...

View File

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

View File

@ -64,9 +64,11 @@ class UserController extends BaseController
*/
public function __construct(UserRepository $user_repo)
{
parent::__construct();
$this->user_repo = $user_repo;
}
/**
@ -156,7 +158,7 @@ class UserController extends BaseController
*/
public function create(CreateUserRequest $request)
{
$user = UserFactory::create(auth()->user()->account->id);
$user = UserFactory::create(auth()->user()->account_id);
return $this->itemResponse($user);
}
@ -208,7 +210,7 @@ class UserController extends BaseController
$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)));
@ -394,7 +396,7 @@ class UserController extends BaseController
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)));
@ -463,9 +465,8 @@ class UserController extends BaseController
*/
public function destroy(DestroyUserRequest $request, User $user)
{
if ($user->isOwner()) {
if($user->isOwner())
return response()->json(['message', 'Cannot detach owner.'],400);
}
/* If the user passes the company user we archive the company user */
$user = $this->user_repo->delete($request->all(), $user);
@ -605,6 +606,7 @@ class UserController extends BaseController
*/
public function detach(DetachCompanyUserRequest $request, User $user)
{
if ($request->entityIsDeleted($user)) {
return $request->disallowUpdate();
}
@ -614,9 +616,8 @@ class UserController extends BaseController
->withTrashed()
->first();
if ($company_user->is_owner) {
if($company_user->is_owner)
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();
@ -680,11 +681,14 @@ class UserController extends BaseController
*/
public function invite(ReconfirmUserRequest $request, User $user)
{
$user->service()->invite($user->company());
return response()->json(['message' => ctrans('texts.confirmation_resent')], 200);
}
/**
* Invite an existing user to a company.
*
@ -734,8 +738,10 @@ class UserController extends BaseController
*/
public function reconfirm(ReconfirmUserRequest $request, User $user)
{
$user->service()->invite($user->company());
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\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Services\ClientPortal\InstantPayment;
@ -43,6 +45,7 @@ class InvitationController extends Controller
public function purchaseOrder(string $invitation_key)
{
Auth::logout();
$invitation = PurchaseOrderInvitation::withTrashed()
@ -53,27 +56,24 @@ class InvitationController extends Controller
->with('contact.vendor')
->first();
if (! $invitation) {
if(!$invitation)
return abort(404,'The resource is no longer available.');
}
if ($invitation->contact->trashed()) {
if($invitation->contact->trashed())
$invitation->contact->restore();
}
$vendor_contact = $invitation->contact;
$entity = 'purchase_order';
if (empty($vendor_contact->email)) {
$vendor_contact->email = Str::random(15).'@example.com';
}
$vendor_contact->save();
if(empty($vendor_contact->email))
$vendor_contact->email = Str::random(15) . "@example.com"; $vendor_contact->save();
if (request()->has('vendor_hash') && request()->input('vendor_hash') == $invitation->contact->vendor->vendor_hash) {
request()->session()->invalidate();
auth()->guard('vendor')->loginUsingId($vendor_contact->id, true);
} else {
nlog('else - default - login contact');
nlog("else - default - login contact");
request()->session()->invalidate();
auth()->guard('vendor')->loginUsingId($vendor_contact->id, true);
}
@ -81,55 +81,49 @@ class InvitationController extends Controller
session()->put('is_silent', request()->has('silent'));
if (auth()->guard('vendor')->user() && ! session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed();
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)]);
}
// 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())
// 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_name = $invitation->purchase_order->numberFormatter().'.pdf';
// $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\VendorLocale;
use App\Http\Middleware\VerifyCsrfToken;
use App\Http\Middleware\VerifyHash;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
@ -161,6 +162,7 @@ class Kernel extends HttpKernel
'locale' => Locale::class,
'vendor_locale' => VendorLocale::class,
'contact_register' => ContactRegister::class,
'verify_hash' => VerifyHash::class,
'shop_token_auth' => ShopTokenAuth::class,
'phantom_secret' => PhantomSecret::class,
'contact_key_login' => ContactKeyLogin::class,

View File

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

View File

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

View File

@ -31,6 +31,7 @@ class PasswordProtection
*/
public function handle($request, Closure $next)
{
$error = [
'message' => 'Invalid Password',
'errors' => new stdClass,
@ -38,61 +39,95 @@ class PasswordProtection
$timeout = auth()->user()->company()->default_password_timeout;
if ($timeout == 0) {
if($timeout == 0)
$timeout = 30*60*1000*1000;
} else {
else
$timeout = $timeout/1000;
}
//test if password if base64 encoded
$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'));
}
// If no password supplied - then we just check if their authentication is in cache //
if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in') && !$x_api_password) {
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
return $next($request);
}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
//todo expand this to include all OAuth providers
if(auth()->user()->oauth_provider_id == 'google')
{
$user = false;
$google = new Google();
$user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD'));
if (is_array($user)) {
$query = [
'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 ($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);
return $next($request);
} elseif ($existing_user = MultiDB::hasUser($query) && ! auth()->user()->company()->oauth_password_required) {
nlog('existing user without password');
nlog("existing user with password");
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
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);
}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);
return $next($request);
} else {
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
{
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;
}

View File

@ -1,4 +1,14 @@
<?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;

View File

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

View File

@ -14,6 +14,7 @@ namespace App\Http\Requests\Expense;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Expense\UniqueExpenseNumberRule;
use App\Models\Expense;
use App\Models\PurchaseOrder;
use App\Utils\Traits\MakesHash;
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)
{
return Client::find($client_id);
return Client::withTrashed()->find($client_id);
}
}

View File

@ -11,14 +11,17 @@
namespace App\Http\Requests\PurchaseOrder;
use App\Http\Requests\Request;
use App\Models\PurchaseOrder;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class StorePurchaseOrderRequest extends Request
{
use MakesHash;
use CleanLineItems;
/**
* 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);
}
/**
* 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['discount'] = 'sometimes|numeric';
$rules['is_amount_discount'] = ['boolean'];
$rules['line_items'] = 'array';
return $rules;
}
public function prepareForValidation()
protected function prepareForValidation()
{
$input = $this->all();
$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);
}
}

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\PurchaseOrder;
use App\Http\Requests\Request;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
@ -20,6 +21,7 @@ class UpdatePurchaseOrderRequest extends Request
{
use ChecksEntityStatus;
use MakesHash;
use CleanLineItems;
/**
* Determine if the user is authorized to make this request.
@ -59,6 +61,10 @@ class UpdatePurchaseOrderRequest extends Request
$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);
}
}

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
/**
* Quote Ninja (https://paymentninja.com).
* Invoice Ninja (https://paymentninja.com).
*
* @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
*/
@ -23,7 +23,7 @@ class SortTaskRequest extends Request
public function authorize() : bool
{
return true;
// return auth()->user()->can('edit', $this->task);
}
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!
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')))
->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\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Mail;
use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Support\Facades\Cache;
/*Multi Mailer implemented*/
@ -65,8 +65,10 @@ class NinjaMailerJob implements ShouldQueue
public function __construct(NinjaMailerObject $nmo, bool $override = false)
{
$this->nmo = $nmo;
$this->override = $override;
}
public function handle()
@ -78,29 +80,30 @@ class NinjaMailerJob implements ShouldQueue
/* Serializing models from other jobs wipes the primary key */
$this->company = Company::where('company_key', $this->nmo->company->company_key)->first();
if ($this->preFlightChecksFail()) {
if($this->preFlightChecksFail())
return;
}
/* Set the email driver */
$this->setMailDriver();
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;
} else {
else
$reply_to_name = $this->nmo->settings->reply_to_email;
}
$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());
}
//send email
try {
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)
->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 */
Cache::increment($this->company->account->key);
} catch (\Exception $e) {
nlog("error failed with {$e->getMessage()}");
$message = $e->getMessage();
@ -130,19 +135,18 @@ class NinjaMailerJob implements ShouldQueue
$message = $message_body->Message;
nlog($message);
}
}
/* If the is an entity attached to the message send a failure mailer */
if ($this->nmo->entity) {
if($this->nmo->entity)
$this->entityEmailFailed($message);
}
/* Don't send postmark failures to Sentry */
if (Ninja::isHosted() && (! $e instanceof ClientException)) {
if(Ninja::isHosted() && (!$e instanceof ClientException))
app('sentry')->captureException($e);
}
}
}
/* Switch statement to handle failure notifications */
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)));
break;
default:
// code...
# code...
break;
}
if ($this->nmo->to_user instanceof ClientContact) {
if ($this->nmo->to_user instanceof ClientContact)
$this->logMailError($message, $this->nmo->to_user->client);
}
}
private function setMailDriver()
{
@ -188,6 +191,7 @@ class NinjaMailerJob implements ShouldQueue
default:
break;
}
}
private function setOfficeMailer()
@ -196,23 +200,35 @@ class NinjaMailerJob implements ShouldQueue
$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()}");
$token = $this->refreshOfficeToken($user);
if ($token) {
if($token)
{
$user->oauth_user_token = $token;
$user->save();
} else {
$this->nmo->settings->email_sending_method = 'default';
}
else {
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$this->nmo
->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
->withSwiftMessage(function ($message) use($token) {
$message->getHeaders()->addTextHeader('GmailToken', $token);
});
@ -221,19 +237,27 @@ class NinjaMailerJob implements ShouldQueue
private function setGmailMailer()
{
if (LaravelGmail::check()) {
if(LaravelGmail::check())
LaravelGmail::logout();
}
$sending_user = $this->nmo->settings->gmail_sending_user_id;
$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();
try{
if ($google->getClient()->isAccessTokenExpired()) {
$google->refreshToken($user);
$user = $user->fresh();
@ -242,20 +266,20 @@ class NinjaMailerJob implements ShouldQueue
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
sleep(rand(2,6));
} catch (\Exception $e) {
}
catch(\Exception $e) {
$this->logMailError('Gmail Token Invalid', $this->company->clients()->first());
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
/**
* If the user doesn't have a valid token, notify them
*/
if(!$user->oauth_user_token) {
$this->company->account->gmailCredentialNotification();
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
@ -270,50 +294,55 @@ class NinjaMailerJob implements ShouldQueue
if(!$token) {
$this->company->account->gmailCredentialNotification();
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$this->nmo
->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
->withSwiftMessage(function ($message) use($token) {
$message->getHeaders()->addTextHeader('GmailToken', $token);
});
}
private function preFlightChecksFail()
{
/* 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;
}
/* 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;
}
/* 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;
}
/* 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;
}
/* 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;
}
/* 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;
}
private function logMailError($errors, $recipient_object)
{
SystemLogger::dispatch(
$errors,
SystemLog::CATEGORY_MAIL,
@ -333,9 +362,14 @@ class NinjaMailerJob implements ShouldQueue
public function failed($exception = null)
{
}
private function refreshOfficeToken($user)
{
$expiry = $user->oauth_user_token_expiry ?: now()->subDay();
if($expiry->lt(now()))
{
$guzzle = new \GuzzleHttp\Client();
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
@ -344,16 +378,37 @@ class NinjaMailerJob implements ShouldQueue
'form_params' => [
'client_id' => config('ninja.o365.client_id') ,
'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',
'refresh_token' => $user->oauth_user_refresh_token,
'refresh_token' => $user->oauth_user_refresh_token
],
])->getBody()->getContents());
nlog($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 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;
public $invitation;
/**
* Create a new job instance.
*
@ -71,19 +70,19 @@ class ProcessPostmarkWebhook implements ShouldQueue
*/
public function handle()
{
MultiDB::findAndSetDbByCompanyKey($this->request['Tag']);
$this->invitation = $this->discoverInvitation($this->request['MessageID']);
if (! $this->invitation) {
if(!$this->invitation)
return;
}
if (array_key_exists('Details', $this->request)) {
if(array_key_exists('Details', $this->request))
$this->invitation->email_error = $this->request['Details'];
}
switch ($this->request['RecordType']) {
switch ($this->request['RecordType'])
{
case 'Delivery':
return $this->processDelivery();
case 'Bounce':
@ -93,9 +92,10 @@ class ProcessPostmarkWebhook implements ShouldQueue
case 'Open':
return $this->processOpen();
default:
// code...
# code...
break;
}
}
// {
@ -137,6 +137,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
private function processOpen()
{
$this->invitation->opened_date = now();
$this->invitation->save();
@ -147,6 +148,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
$this->invitation->contact->client,
$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);
if (config('ninja.notification.slack')) {
$this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja();
}
// if(config('ninja.notification.slack'))
// $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja();
}
// {
@ -250,6 +252,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
// }
private function processSpamComplaint()
{
$this->invitation->email_status = 'spam';
$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);
if (config('ninja.notification.slack')) {
if(config('ninja.notification.slack'))
$this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja();
}
}
private function discoverInvitation($message_id)
{
$invitation = false;
if ($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) {
if($invitation = InvoiceInvitation::where('message_id', $message_id)->first())
return $invitation;
} elseif ($invitation = QuoteInvitation::where('message_id', $message_id)->first()) {
elseif($invitation = QuoteInvitation::where('message_id', $message_id)->first())
return $invitation;
} elseif ($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) {
elseif($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first())
return $invitation;
} elseif ($invitation = CreditInvitation::where('message_id', $message_id)->first()) {
elseif($invitation = CreditInvitation::where('message_id', $message_id)->first())
return $invitation;
} else {
else
return $invitation;
}
}
}

View File

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

View File

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

View File

@ -39,7 +39,7 @@ class VersionCheck implements ShouldQueue
nlog("latest version = {$version_file}");
if ($version_file) {
if (Ninja::isSelfHost() && $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\MakesInvoiceHtml;
use App\Utils\Traits\NumberFormatter;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Utils\Traits\Pdf\PDF;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Utils\Traits\Pdf\PdfMaker;
use App\Utils\VendorHtmlEngine;
use Illuminate\Bus\Queueable;
@ -65,6 +65,10 @@ class CreatePurchaseOrderPdf implements ShouldQueue
public $vendor;
private string $path = '';
private string $file_path = '';
/**
* Create a new job instance.
*
@ -84,10 +88,38 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$this->vendor->load('company');
$this->disk = $disk ?? config('filesystems.default');
}
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);
/* Forget the singleton*/
@ -107,25 +139,24 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$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';
$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');
$design = Design::find($entity_design_id);
/* Catch all in case migration doesn't pass back a valid design */
if (! $design) {
if(!$design)
$design = Design::find(2);
}
$html = new VendorHtmlEngine($this->invitation);
if ($design->is_custom) {
$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);
} else {
@ -160,23 +191,28 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$pdf = null;
try {
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, $this->company);
if ($numbered_pdf) {
if($numbered_pdf)
$pdf = $numbered_pdf;
}
} else {
else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, $this->company);
if ($numbered_pdf) {
if($numbered_pdf)
$pdf = $numbered_pdf;
}
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}
@ -185,22 +221,14 @@ class CreatePurchaseOrderPdf implements ShouldQueue
info($maker->getCompiledHTML());
}
if ($pdf) {
try {
if (! Storage::disk($this->disk)->exists($path)) {
Storage::disk($this->disk)->makeDirectory($path, 0775);
}
return $pdf;
Storage::disk($this->disk)->put($file_path, $pdf, 'public');
} catch (\Exception $e) {
throw new FilePermissionsFailure($e->getMessage());
}
}
return $file_path;
}
public function failed($e)
{
}
}

View File

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

View File

@ -56,6 +56,7 @@ class Expense extends BaseModel
'tax_amount3',
'uses_inclusive_taxes',
'calculate_tax_by_amount',
'purchase_order_id',
];
protected $casts = [
@ -102,6 +103,11 @@ class Expense extends BaseModel
return $this->belongsTo(Client::class);
}
public function purchase_order()
{
return $this->hasOne(PurchaseOrder::class);
}
public function translate_entity()
{
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';
} elseif ($this->id == 20 || $this->id == 56) {
$link = 'https://dashboard.stripe.com/account/apikeys';
} elseif ($this->id == 59) {
$link = 'https://www.forte.net/';
}
return $link;
@ -168,6 +170,12 @@ class Gateway extends StaticModel
GatewayType::HOSTED_PAGE => ['refund' => false, 'token_billing' => false, 'webhooks' => [' ']], // Razorpay
];
break;
case 59:
return [
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], // Forte
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']],
];
break;
default:
return [];
break;

View File

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

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Jobs\Entity\CreateEntityPdf;
@ -33,7 +34,6 @@ class PurchaseOrder extends BaseModel
'discount',
'company_id',
'status_id',
'user_id',
'last_sent_date',
'is_deleted',
'po_number',
@ -72,10 +72,6 @@ class PurchaseOrder extends BaseModel
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
// 'custom_surcharge_tax1',
// 'custom_surcharge_tax2',
// 'custom_surcharge_tax3',
// 'custom_surcharge_tax4',
'design_id',
'invoice_id',
'assigned_user_id',
@ -83,9 +79,8 @@ class PurchaseOrder extends BaseModel
'balance',
'partial',
'paid_to_date',
// 'subscription_id',
'vendor_id',
'last_viewed',
'last_viewed'
];
protected $casts = [
@ -99,12 +94,10 @@ class PurchaseOrder extends BaseModel
];
const STATUS_DRAFT = 1;
const STATUS_SENT = 2;
const STATUS_ACCEPTED = 3;
const STATUS_CANCELLED = 4;
const STATUS_RECEIVED = 4;
const STATUS_CANCELLED = 5;
public static function stringStatus(int $status)
{
@ -126,6 +119,7 @@ class PurchaseOrder extends BaseModel
}
}
public static function badgeForStatus(int $status)
{
switch ($status) {
@ -147,6 +141,7 @@ class PurchaseOrder extends BaseModel
}
}
public function assigned_user()
{
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
@ -154,7 +149,7 @@ class PurchaseOrder extends BaseModel
public function vendor()
{
return $this->belongsTo(Vendor::class);
return $this->belongsTo(Vendor::class)->withTrashed();
}
public function history()
@ -172,6 +167,11 @@ class PurchaseOrder extends BaseModel
return $this->belongsTo(Company::class);
}
public function expense()
{
return $this->belongsTo(Expense::class);
}
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
@ -181,7 +181,6 @@ class PurchaseOrder extends BaseModel
{
return $this->belongsTo(Client::class)->withTrashed();
}
public function markInvitationsSent()
{
$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)
{
if (! $invitation) {
if ($this->invitations()->exists()) {
if($this->invitations()->exists())
$invitation = $this->invitations()->first();
} else {
else{
$this->service()->createInvitations();
$invitation = $this->invitations()->first();
}
}
if (! $invitation) {
if(!$invitation)
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';
if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($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);
}
if (Storage::disk('public')->exists($file_path)) {
if(Storage::disk('public')->exists($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);
}
@ -272,4 +271,5 @@ class PurchaseOrder extends BaseModel
return $purchase_order_calc->build();
}
}

View File

@ -104,6 +104,7 @@ class User extends Authenticatable implements MustVerifyEmail
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'oauth_user_token_expiry' => 'datetime',
];
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
{
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' => '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'],
@ -75,6 +87,9 @@ class AuthorizePaymentDriver extends BaseDriver
['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'],
];
return array_merge($fields, $data);
}
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()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
$company = Company::where('company_key', $this->company_key)->first();
foreach ($this->stripe_request as $transaction) {
if (array_key_exists('payment_intent', $transaction)) {
if(array_key_exists('payment_intent', $transaction))
{
$payment = Payment::query()
->where('company_id', $company->id)
->where(function ($query) use ($transaction) {
@ -66,7 +69,9 @@ class PaymentIntentWebhook implements ShouldQueue
->orWhere('transaction_reference', $transaction['id']);
})
->first();
} else {
}
else
{
$payment = Payment::query()
->where('company_id', $company->id)
->where('transaction_reference', $transaction['id'])
@ -81,11 +86,13 @@ class PaymentIntentWebhook implements ShouldQueue
}
}
if ($this->payment_completed) {
if($this->payment_completed)
return;
}
if(optional($this->stripe_request['object']['charges']['data'][0])['id']){
$company = Company::where('company_key', $this->company_key)->first();
$payment = Payment::query()
@ -95,27 +102,28 @@ class PaymentIntentWebhook implements ShouldQueue
//return early
if($payment && $payment->status_id == Payment::STATUS_COMPLETED){
nlog(' payment found and status correct - returning ');
nlog(" payment found and status correct - returning ");
return;
} elseif ($payment) {
}
elseif($payment){
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
}
$hash = optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['payment_hash'];
$payment_hash = PaymentHash::where('hash', $hash)->first();
if (! $payment_hash) {
if(!$payment_hash)
return;
}
nlog('payment intent');
nlog("payment intent");
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'])) {
nlog('hash found');
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");
$hash = $this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash'];
@ -125,7 +133,21 @@ class PaymentIntentWebhook implements ShouldQueue
$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)
@ -156,5 +178,7 @@ class PaymentIntentWebhook implements ShouldQueue
$client,
$client->company,
);
}
}

View File

@ -32,7 +32,6 @@ class BaseRepository
public bool $import_mode = false;
private bool $new_model = false;
/**
* @param $entity
* @param $type
@ -138,9 +137,8 @@ class BaseRepository
/* Returns an invoice if defined as a key in the $resource array*/
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;
}
$invitation_class = sprintf('App\\Models\\%sInvitation', $resource);
@ -153,13 +151,13 @@ class BaseRepository
private function resolveEntityKey($model)
{
switch ($model) {
case $model instanceof RecurringInvoice:
case ($model instanceof RecurringInvoice):
return 'recurring_invoice_id';
case $model instanceof Invoice:
case ($model instanceof Invoice):
return 'invoice_id';
case $model instanceof Quote:
case ($model instanceof Quote):
return 'quote_id';
case $model instanceof Credit:
case ($model instanceof Credit):
return 'credit_id';
}
}
@ -175,9 +173,8 @@ class BaseRepository
protected function alternativeSave($data, $model)
{
//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'];
}
$client = Client::where('id', $model->client_id)->withTrashed()->firstOrFail();
@ -198,13 +195,11 @@ class BaseRepository
$tmp_data = $data; //preserves the $data array
/* 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']);
}
if (isset($tmp_data['client_contacts'])) {
if (isset($tmp_data['client_contacts']))
unset($tmp_data['client_contacts']);
}
$model->fill($tmp_data);
@ -213,26 +208,22 @@ class BaseRepository
$model->custom_surcharge_tax3 = $client->company->custom_surcharge_taxes3;
$model->custom_surcharge_tax4 = $client->company->custom_surcharge_taxes4;
if (! $model->id) {
if(!$model->id)
$this->new_model = true;
}
$model->saveQuietly();
/* Model now persisted, now lets do some child tasks */
if ($model instanceof Invoice) {
if($model instanceof Invoice)
$model->service()->setReminder()->save();
}
/* Save any documents */
if (array_key_exists('documents', $data)) {
if (array_key_exists('documents', $data))
$this->saveDocuments($data['documents'], $model);
}
if (array_key_exists('file', $data)) {
if (array_key_exists('file', $data))
$this->saveDocuments($data['file'], $model);
}
/* If invitations are present we need to filter existing invitations with the new ones */
if (isset($data['invitations'])) {
@ -243,23 +234,24 @@ class BaseRepository
$invitation_class = sprintf('App\\Models\\%sInvitation', $resource);
$invitation = $invitation_class::where('key', $invitation)->first();
if ($invitation) {
if ($invitation)
$invitation->delete();
}
});
foreach ($data['invitations'] as $invitation) {
//if no invitations are present - create one.
if (! $this->getInvitation($invitation, $resource)) {
if (isset($invitation['id'])) {
if (isset($invitation['id']))
unset($invitation['id']);
}
//make sure we are creating an invite for a contact who belongs to the client only!
$contact = ClientContact::find($invitation['client_contact_id']);
if ($contact && $model->client_id == $contact->client_id) {
$invitation_class = sprintf('App\\Models\\%sInvitation', $resource);
$new_invitation = $invitation_class::withTrashed()
@ -268,14 +260,18 @@ class BaseRepository
->first();
if ($new_invitation && $new_invitation->trashed()) {
$new_invitation->restore();
} else {
$invitation_factory_class = sprintf('App\\Factory\\%sInvitationFactory', $resource);
$new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id);
$new_invitation->{$lcfirst_resource_id} = $model->id;
$new_invitation->client_contact_id = $contact->id;
$new_invitation->key = $this->createDbHash($model->company->db);
$new_invitation->save();
}
}
}
@ -283,9 +279,8 @@ class BaseRepository
}
/* 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();
}
/* Recalculate invoice amounts */
$model = $model->calc()->getInvoice();
@ -297,82 +292,83 @@ class BaseRepository
$model = $model->service()->applyNumber()->save();
/* 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);
}
/* 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);
}
/* Perform model specific tasks */
if ($model instanceof Invoice) {
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {
//10-07-2022
$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->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'));
}
//links tasks and expenses back to the invoice.
$model->service()->linkEntities()->save();
if ($this->new_model) {
if($this->new_model)
event('eloquent.created: App\Models\Invoice', $model);
} else {
else
event('eloquent.updated: App\Models\Invoice', $model);
}
}
if ($model instanceof Credit) {
$model = $model->calc()->getCredit();
if (! $model->design_id) {
if (! $model->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'];
}
if ($this->new_model) {
if($this->new_model)
event('eloquent.created: App\Models\Credit', $model);
} else {
else
event('eloquent.updated: App\Models\Credit', $model);
}
}
if ($model instanceof Quote) {
if (! $model->design_id) {
if (! $model->design_id)
$model->design_id = $this->decodePrimaryKey($client->getSetting('quote_design_id'));
}
$model = $model->calc()->getQuote();
if ($this->new_model) {
if($this->new_model)
event('eloquent.created: App\Models\Quote', $model);
} else {
else
event('eloquent.updated: App\Models\Quote', $model);
}
}
if ($model instanceof RecurringInvoice) {
if (! $model->design_id) {
if (! $model->design_id)
$model->design_id = $this->decodePrimaryKey($client->getSetting('invoice_design_id'));
}
$model = $model->calc()->getRecurringInvoice();
if ($this->new_model) {
if($this->new_model)
event('eloquent.created: App\Models\RecurringInvoice', $model);
} else {
else
event('eloquent.updated: App\Models\RecurringInvoice', $model);
}
}
$model->save();

View File

@ -30,15 +30,13 @@ use Illuminate\Support\Carbon;
/**
* PaymentRepository.
*/
class PaymentRepository extends BaseRepository
{
class PaymentRepository extends BaseRepository {
use MakesHash;
use SavesDocuments;
protected $credit_repo;
public function __construct(CreditRepository $credit_repo)
{
public function __construct( CreditRepository $credit_repo ) {
$this->credit_repo = $credit_repo;
}
@ -62,6 +60,7 @@ class PaymentRepository extends BaseRepository
*/
private function applyPayment(array $data, Payment $payment): ?Payment
{
$is_existing_payment = true;
$client = false;
@ -70,9 +69,8 @@ class PaymentRepository extends BaseRepository
$payment = $this->processExchangeRates($data, $payment);
/* 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']);
}
$is_existing_payment = false;
$client = Client::where('id', $data['client_id'])->withTrashed()->first();
@ -85,7 +83,7 @@ class PaymentRepository extends BaseRepository
$client->service()->updatePaidToDate($data['amount'])->save();
}
// elseif($data['amount'] >0){
else{
//this fixes an edge case with unapplied payments
$client->service()->updatePaidToDate($data['amount'])->save();
@ -95,7 +93,9 @@ class PaymentRepository extends BaseRepository
$_credit_totals = array_sum(array_column($data['credits'], 'amount'));
$client->service()->updatePaidToDate($_credit_totals)->save();
}
}
/*Fill the payment*/
@ -104,11 +104,12 @@ class PaymentRepository extends BaseRepository
$payment->status_id = Payment::STATUS_COMPLETED;
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;
} else {
else
$payment->currency_id = $client->company->settings->currency_id;
}
}
$payment->save();
@ -129,6 +130,7 @@ class PaymentRepository extends BaseRepository
/*Iterate through invoices and apply payments*/
if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) {
$invoice_totals = array_sum(array_column($data['invoices'], 'amount'));
$invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get();
@ -164,17 +166,17 @@ class PaymentRepository extends BaseRepository
if ($credit) {
$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 (array_key_exists('email_receipt', $data) && $data['email_receipt'] == 'true') {
if (array_key_exists('email_receipt', $data) && $data['email_receipt'] == 'true')
$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();
}
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)
{
if(array_key_exists('exchange_rate', $data) && isset($data['exchange_rate']) && $data['exchange_rate'] != 1){
return $payment;
}
@ -215,6 +218,7 @@ class PaymentRepository extends BaseRepository
$company_currency = $client->company->settings->currency_id;
if ($company_currency != $client_currency) {
$exchange_rate = new CurrencyApi();
$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;
public int $quantity = 1;
public function save($data, Subscription $subscription): ?Subscription
{
$subscription->fill($data);
@ -119,7 +121,7 @@ class SubscriptionRepository extends BaseRepository
private function makeLineItem($product, $multiplier)
{
$item = new InvoiceItem;
$item->quantity = $product->quantity;
$item->quantity = $this->quantity;
$item->product_key = $product->product_key;
$item->notes = $product->notes;
$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 */
$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);
});

View File

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

View File

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

View File

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

View File

@ -43,12 +43,10 @@ class ApplyNumber extends AbstractService
switch ($this->client->getSetting('counter_number_applied')) {
case 'when_saved':
$this->trySaving();
// $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id);
break;
case 'when_sent':
if ($this->invoice->status_id == Invoice::STATUS_SENT) {
$this->trySaving();
// $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id);
}
break;
@ -61,21 +59,30 @@ class ApplyNumber extends AbstractService
private function trySaving()
{
$x=1;
do{
try{
$this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id);
$this->invoice->saveQuietly();
$this->completed = false;
} catch (QueryException $e) {
}
catch(QueryException $e){
$x++;
if ($x > 10) {
if($x>10)
$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\Jobs\Invoice\InvoiceWorkflowSettings;
use App\Jobs\Ninja\TransactionLog;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
@ -43,20 +44,17 @@ class UpdateInvoicePayment
$client = $this->payment->client;
if ($client->trashed()) {
if($client->trashed())
$client->restore();
}
collect($paid_invoices)->each(function ($paid_invoice) use ($invoices, $client) {
$client = $client->fresh();
$invoice = $invoices->first(function ($inv) use ($paid_invoice) {
return $paid_invoice->invoice_id == $inv->hashed_id;
});
if ($invoice->trashed()) {
if($invoice->trashed())
$invoice->restore();
}
if ($invoice->id == $this->payment_hash->fee_invoice_id) {
$paid_amount = $paid_invoice->amount + $this->payment_hash->fee_total;
@ -64,14 +62,19 @@ class UpdateInvoicePayment
$paid_amount = $paid_invoice->amount;
}
$client->paid_to_date += $paid_amount;
$client->balance -= $paid_amount;
$client->save();
\DB::connection(config('database.default'))->transaction(function () use($client, $paid_amount){
$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 */
if ($paid_amount > $invoice->partial && $paid_amount > $invoice->balance) {
if($paid_amount > $invoice->partial && $paid_amount > $invoice->balance)
$paid_amount = $invoice->balance;
}
/*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!
@ -115,6 +118,8 @@ class UpdateInvoicePayment
];
TransactionLog::dispatch(TransactionEvent::GATEWAY_PAYMENT_MADE, $transaction, $invoice->company->db);
});
/* Remove the event updater from within the loop to prevent race conditions */
@ -122,7 +127,9 @@ class UpdateInvoicePayment
$this->payment->saveQuietly();
$invoices->each(function ($invoice) {
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
});
return $this->payment->fresh();

View File

@ -60,40 +60,33 @@ class Design extends BaseDesign
public $company;
public $client_or_vendor_entity;
/** @var array */
public $aging = [];
const BOLD = 'bold';
const BUSINESS = 'business';
const CLEAN = 'clean';
const CREATIVE = 'creative';
const ELEGANT = 'elegant';
const HIPSTER = 'hipster';
const MODERN = 'modern';
const PLAIN = 'plain';
const PLAYFUL = 'playful';
const CUSTOM = 'custom';
const DELIVERY_NOTE = 'delivery_note';
const STATEMENT = 'statement';
const PURCHASE_ORDER = 'purchase_order';
public function __construct(string $design = null, array $options = [])
{
Str::endsWith('.html', $design) ? $this->design = $design : $this->design = "{$design}.html";
$this->options = $options;
}
public function html(): ?string
@ -182,9 +175,27 @@ class Design extends BaseDesign
$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
{
$variables = $this->context['pdf_variables']['company_details'];
@ -215,9 +226,8 @@ class Design extends BaseDesign
{
$elements = [];
if (! $this->vendor) {
if(!$this->vendor)
return $elements;
}
$variables = $this->context['pdf_variables']['vendor_details'];
@ -232,9 +242,8 @@ class Design extends BaseDesign
{
$elements = [];
if (! $this->client) {
if(!$this->client)
return $elements;
}
if ($this->type == self::DELIVERY_NOTE) {
$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_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'])) {
@ -268,13 +277,16 @@ class Design extends BaseDesign
public function entityDetails(): array
{
if ($this->type === 'statement') {
$s_date = $this->translateDate(now(), $this->client->date_format(), $this->client->locale());
return [
['element' => 'tr', 'properties' => ['data-ref' => 'statement-label'], 'elements' => [
['element' => 'th', 'properties' => [], 'content' => ''],
['element' => 'th', 'properties' => [], 'content' => '<h2>'.ctrans('texts.statement').'</h2>'],
['element' => 'th', 'properties' => [], 'content' => ""],
['element' => 'th', 'properties' => [], 'content' => "<h2>".ctrans('texts.statement')."</h2>"],
]],
['element' => 'tr', 'properties' => [], 'elements' => [
['element' => 'th', 'properties' => [], 'content' => ctrans('texts.statement_date')],
@ -302,7 +314,9 @@ class Design extends BaseDesign
}
if($this->vendor){
$variables = $this->context['pdf_variables']['purchase_order_details'];
}
$elements = [];
@ -318,7 +332,10 @@ class Design extends BaseDesign
$_variable = explode('.', $variable)[1];
$_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' => [
['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)]],
@ -495,12 +512,13 @@ class Design extends BaseDesign
// }
// }
//24-03-2022 show payments per invoice
foreach ($this->invoices as $invoice) {
foreach ($invoice->payments as $payment) {
if ($payment->is_deleted) {
if($payment->is_deleted)
continue;
}
$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;'];
$tbody[] = $element;
}
}
@ -730,12 +749,13 @@ class Design extends BaseDesign
$variables = $this->context['pdf_variables']['total_columns'];
$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' => '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' => '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' => []],
];
if ($this->type == self::DELIVERY_NOTE) {
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) {
$variable = sprintf('%s%s', '$', $property);
@ -787,7 +816,7 @@ class Design extends BaseDesign
foreach ($taxes as $i => $tax) {
$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' => 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') {
@ -800,13 +829,13 @@ class Design extends BaseDesign
foreach ($taxes as $i => $tax) {
$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' => 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')) {
$_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' => [
['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' => [
['element' => 'span', 'content' => ''],
['element' => 'span', 'content' => '',],
['element' => 'span', 'content' => ''],
]];

View File

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

View File

@ -40,7 +40,20 @@ class ApplyNumber extends AbstractService
return $this->purchase_order;
}
switch ($this->vendor->company->getSetting('counter_number_applied')) {
case 'when_saved':
$this->trySaving();
break;
case 'when_sent':
if ($this->invoice->status_id == PurchaseOrder::STATUS_SENT) {
$this->trySaving();
}
break;
default:
break;
}
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\CreateInvitations;
use App\Services\PurchaseOrder\GetPurchaseOrderPdf;
use App\Services\PurchaseOrder\PurchaseOrderExpense;
use App\Services\PurchaseOrder\TriggeredActions;
use App\Utils\Traits\MakesHash;
@ -32,6 +33,7 @@ class PurchaseOrderService
public function createInvitations()
{
$this->purchase_order = (new CreateInvitations($this->purchase_order))->run();
return $this;
@ -39,25 +41,34 @@ class PurchaseOrderService
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;
}
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 (!isset($this->purchase_order->exchange_rate) && $this->purchase_order->client->currency()->id != (int)$this->purchase_order->company->settings->currency_id)
// $this->purchase_order->exchange_rate = $this->purchase_order->client->currency()->exchange_rate;
if (! $this->purchase_order->design_id)
$this->purchase_order->design_id = $this->decodePrimaryKey($settings->invoice_design_id);
// if (!isset($this->purchase_order->public_notes))
// $this->purchase_order->public_notes = $this->purchase_order->client->public_notes;
if (!isset($this->invoice->footer) || empty($this->invoice->footer))
$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;
}
public function triggeredActions($request)
@ -89,9 +100,11 @@ class PurchaseOrderService
public function touchPdf($force = false)
{
try {
if($force){
$this->purchase_order->invitations->each(function ($invitation) {
CreatePurchaseOrderPdf::dispatchSync($invitation);
CreatePurchaseOrderPdf::dispatchNow($invitation);
});
return $this;
@ -100,13 +113,39 @@ class PurchaseOrderService
$this->purchase_order->invitations->each(function ($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;
}
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.
* @return \App\Models\PurchaseOrder object
@ -117,4 +156,5 @@ class PurchaseOrderService
return $this->purchase_order;
}
}

View File

@ -11,6 +11,7 @@
namespace App\Services\Recurring;
use App\Jobs\RecurringInvoice\SendRecurring;
use App\Jobs\Util\UnlinkFile;
use App\Models\RecurringInvoice;
use App\Services\Recurring\GetInvoicePdf;
@ -34,9 +35,8 @@ class RecurringService
*/
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;
}
return $this;
}
@ -50,6 +50,7 @@ class RecurringService
public function start()
{
if ($this->recurring_entity->remaining_cycles == 0) {
return $this;
}
@ -84,15 +85,20 @@ class RecurringService
public function deletePdf()
{
$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;
}
public function triggeredActions($request)
{
if ($request->has('start') && $request->input('start') == 'true') {
$this->start();
}
@ -101,7 +107,12 @@ class RecurringService
$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();
$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;
}
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()
{
return $this;
}

View File

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

View File

@ -87,6 +87,7 @@ class AccountTransformer extends EntityTransformer
'hosted_company_count' => (int) $account->hosted_company_count,
'is_hosted' => (bool) Ninja::isHosted(),
'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($account->default_company->owner(), $transformer, User::class);
}
}

View File

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

View File

@ -11,8 +11,10 @@
namespace App\Transformers;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Transformers\DocumentTransformer;
use App\Utils\Traits\MakesHash;
class PurchaseOrderTransformer extends EntityTransformer
@ -21,6 +23,11 @@ class PurchaseOrderTransformer extends EntityTransformer
protected $defaultIncludes = [
'invitations',
'documents'
];
protected $availableIncludes = [
'expense'
];
public function includeInvitations(PurchaseOrder $purchase_order)
@ -30,6 +37,21 @@ class PurchaseOrderTransformer extends EntityTransformer
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)
{
return [
@ -88,10 +110,12 @@ class PurchaseOrderTransformer extends EntityTransformer
'custom_surcharge_tax3' => (bool)$purchase_order->custom_surcharge_tax3,
'custom_surcharge_tax4' => (bool)$purchase_order->custom_surcharge_tax4,
'line_items' => $purchase_order->line_items ?: (array)[],
'entity_type' => 'purchase_order',
'entity_type' => 'purchaseOrder',
'exchange_rate' => (float)$purchase_order->exchange_rate,
'paid_to_date' => (float)$purchase_order->paid_to_date,
'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\Task;
use App\Models\TaskStatus;
use App\Transformers\TaskStatusTransformer;
use App\Utils\Traits\MakesHash;
use League\Fractal\Resource\Item;
/**
* class TaskTransformer.
@ -30,6 +33,8 @@ class TaskTransformer extends EntityTransformer
* @var array
*/
protected $availableIncludes = [
'client',
'status'
];
public function includeDocuments(Task $task)
@ -39,6 +44,27 @@ class TaskTransformer extends EntityTransformer
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)
{
return [

View File

@ -12,6 +12,7 @@
namespace App\Utils;
use App\Helpers\SwissQr\SwissQrGenerator;
use App\Models\Country;
use App\Models\CreditInvitation;
use App\Models\GatewayType;
@ -160,6 +161,17 @@ class HtmlEngine
if ($this->entity->vendor) {
$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') {
@ -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['$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.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')];

View File

@ -16,16 +16,22 @@ use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\MakesTemplateData;
use App\Utils\VendorHtmlEngine;
use DB;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Str;
use League\CommonMark\CommonMarkConverter;
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;
@ -79,6 +85,7 @@ class TemplateEngine
public function build()
{
return $this->setEntity()
->setSettingsObject()
->setTemplates()
@ -89,7 +96,7 @@ class TemplateEngine
private function setEntity()
{
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();
} else {
$this->mockEntity();
@ -100,7 +107,11 @@ class TemplateEngine
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 = $this->settings_entity->getMergedSettings();
} else {
@ -144,9 +155,13 @@ class TemplateEngine
$this->raw_body = $this->body;
$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());
} else {
}
else {
$this->fakerValues();
}
@ -168,16 +183,19 @@ class TemplateEngine
'allow_unsafe_links' => false,
]);
$this->body = $converter->convert($this->body);
$this->body = $converter->convert($this->body)->getContent();
}
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->body = strtr($this->body, $this->labels_and_values['labels']);
$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['values']);
@ -199,7 +217,18 @@ class TemplateEngine
$data['footer'] = '';
$data['logo'] = auth()->user()->company()->present()->logo();
if($this->entity_obj->client()->exists())
$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') {
$wrapper = $this->settings_entity->getSetting('email_style_custom');
@ -240,8 +269,13 @@ class TemplateEngine
private function mockEntity()
{
if(!$this->entity && $this->template && str_contains($this->template, 'purchase_order'))
$this->entity = 'purchaseOrder';
DB::connection(config('database.default'))->beginTransaction();
$vendor = false;
$client = Client::factory()->create([
'user_id' => auth()->user()->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('client', $client);
$this->entity_obj->setRelation('company', auth()->user()->company());
@ -292,6 +373,7 @@ class TemplateEngine
$client->setRelation('company', auth()->user()->company());
$client->load('company');
}
}
private function tearDown()
{

View File

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

View File

@ -114,6 +114,9 @@ trait CompanySettingsSaver
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') {
$value = 'integer';
if($key == 'besr_id')
$value = 'string';
if (! property_exists($settings, $key)) {
continue;
} elseif (! $this->checkAttribute($value, $settings->{$key})) {
@ -182,6 +185,9 @@ trait CompanySettingsSaver
$value = 'string';
}
if($key == 'besr_id')
$value = 'string';
if (! property_exists($settings, $key)) {
continue;
} 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.line_total'] = ['value' => '$100.00', 'label' => ctrans('texts.line_total')];
//$data['$paid_to_date'] = ;
// $data['$your_invoice'] = ;
// $data['$quote'] = ;
// $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['$vendor_name'] = ['value' => 'Joey Diaz Denkins', 'label' => ctrans('texts.vendor_name')];;
$data['$vendor.name'] = &$data['$vendor_name'];
$data['$vendor'] = &$data['$vendor_name'];
$data['$outstanding'] = ;
$data['$invoice_due_date'] = ;
$data['$quote_due_date'] = ;
$data['$service'] = ;
$data['$product_key'] = ;
$data['$unit_cost'] = ;
$data['$custom_value1'] = ;
$data['$custom_value2'] = ;
$data['$delivery_note'] = ;
$data['$date'] = ;
$data['$method'] = ;
$data['$payment_date'] = ;
$data['$reference'] = ;
$data['$amount'] = ;
$data['$amount_paid'] =;
*/
$data['$vendor.address1'] = &$data['$address1'];
$data['$vendor.address2'] = &$data['$address2'];
$data['$vendor_address'] = ['value' => '5 Kalamazoo Way\n Jimbuckeroo\n USA 90210', 'label' => ctrans('texts.address')];
$data['$vendor.address'] = &$data['$vendor_address'];
$data['$vendor.postal_code'] = ['value' => '90210', 'label' => ctrans('texts.postal_code')];
$data['$vendor.public_notes'] = $data['$invoice.public_notes'];
$data['$vendor.city'] = &$data['$company.city'];
$data['$vendor.state'] = &$data['$company.state'];
$data['$vendor.id_number'] = &$data['$id_number'];
$data['$vendor.vat_number'] = &$data['$vat_number'];
$data['$vendor.website'] = &$data['$website'];
$data['$vendor.phone'] = &$data['$phone'];
$data['$vendor.city_state_postal'] = &$data['$city_state_postal'];
$data['$vendor.postal_city_state'] = &$data['$postal_city_state'];
$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));
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)) {
$value = 'integer';
if ($key == 'gmail_sending_user_id') {
if($key == 'gmail_sending_user_id' || $key == 'besr_id')
$value = 'string';
}
if (! property_exists($settings, $key)) {
continue;

View File

@ -63,6 +63,11 @@ class VendorHtmlEngine
$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->settings = $this->company->settings;
@ -72,28 +77,29 @@ class VendorHtmlEngine
$this->helpers = new Helpers();
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function resolveEntityString()
{
switch ($this->invitation) {
case $this->invitation instanceof InvoiceInvitation:
case ($this->invitation instanceof InvoiceInvitation):
return 'invoice';
break;
case $this->invitation instanceof CreditInvitation:
case ($this->invitation instanceof CreditInvitation):
return 'credit';
break;
case $this->invitation instanceof QuoteInvitation:
case ($this->invitation instanceof QuoteInvitation):
return 'quote';
break;
case $this->invitation instanceof RecurringInvoiceInvitation:
case ($this->invitation instanceof RecurringInvoiceInvitation):
return 'recurring_invoice';
break;
case $this->invitation instanceof PurchaseOrderInvitation:
case ($this->invitation instanceof PurchaseOrderInvitation):
return 'purchase_order';
break;
default:
// code...
# code...
break;
}
}
@ -157,23 +163,25 @@ class VendorHtmlEngine
$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')];
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')];
} else {
else
$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) {
$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['$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')];
} else {
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_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.balance_due')];
$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_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
$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['$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['$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['$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['$vendor_name'] = ['value' => $this->vendor->present()->name() ?: '&nbsp;', 'label' => ctrans('texts.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['$auto_bill'] = &$data['$autoBill'];
$data['$dir'] = ['value' => $this->company->language()?->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];
$data['$dir_text_align'] = ['value' => $this->company->language()?->locale === 'ar' ? 'right' : 'left', 'label' => ''];
$data['$dir'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'rtl' : 'ltr', '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['$method'] = ['value' => '&nbsp;', 'label' => ctrans('texts.method')];
@ -483,20 +490,19 @@ class VendorHtmlEngine
return '&nbsp;';
}
private function getCountryCode() :string
{
$country = Country::find($this->settings->country_id);
if ($country) {
if($country)
return $country->iso_3166_2;
}
// if ($country) {
// return ctrans('texts.country_' . $country->iso_3166_2);
// }
return '&nbsp;';
}
/**
* Due to the way we are compiling the blade template we
* have no ability to iterate, so in the case

View File

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

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.4.4',
'app_tag' => '5.4.4',
'app_version' => '5.4.10',
'app_tag' => '5.4.10',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),
@ -194,4 +194,7 @@ return [
'ninja_apple_bundle_id' => env('APPLE_BUNDLE_ID', false),
'ninja_apple_issuer_id' => env('APPLE_ISSUER_ID', 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