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
@ -213,7 +221,7 @@ class InvoiceFilters extends QueryFilters
{
if (auth()->guard('contact')->user()) {
return $this->contactViewFilter();
} else {
} else {
return $this->builder->company()->with(['invitations.company'], ['documents.company']);
}

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)
@ -685,9 +690,9 @@ class LoginController extends BaseController
$parameters = ['access_type' => 'offline', 'prompt' => 'consent select_account', 'redirect_uri' => config('ninja.app_url') . '/auth/google'];
}
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'];
if($provider == '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

@ -60,51 +60,52 @@ class BaseController extends Controller
protected $manager;
private $first_load = [
'account',
'user.company_user',
'token.company_user',
'company.activities',
'company.designs.company',
'company.task_statuses',
'company.expense_categories',
'company.documents',
'company.users.company_user',
'company.clients.contacts.company',
'company.clients.gateway_tokens',
'company.clients.documents',
'company.company_gateways.gateway',
'company.credits.invitations.contact',
'company.credits.invitations.company',
'company.credits.documents',
'company.expenses.documents',
'company.groups.documents',
'company.invoices.invitations.contact',
'company.invoices.invitations.company',
'company.invoices.documents',
'company.products',
'company.products.documents',
'company.payments.paymentables',
'company.payments.documents',
'company.purchase_orders.documents',
'company.payment_terms.company',
'company.projects.documents',
'company.recurring_expenses',
'company.recurring_invoices',
'company.recurring_invoices.invitations.contact',
'company.recurring_invoices.invitations.company',
'company.recurring_invoices.documents',
'company.quotes.invitations.contact',
'company.quotes.invitations.company',
'company.quotes.documents',
'company.tasks.documents',
'company.subscriptions',
'company.tax_rates',
'company.tokens_hashed',
'company.vendors.contacts.company',
'company.vendors.documents',
'company.webhooks',
'company.system_logs',
];
'account',
'user.company_user',
'token.company_user',
'company.activities',
'company.designs.company',
'company.task_statuses',
'company.expense_categories',
'company.documents',
'company.users.company_user',
'company.clients.contacts.company',
'company.clients.gateway_tokens',
'company.clients.documents',
'company.company_gateways.gateway',
'company.credits.invitations.contact',
'company.credits.invitations.company',
'company.credits.documents',
'company.expenses.documents',
'company.groups.documents',
'company.invoices.invitations.contact',
'company.invoices.invitations.company',
'company.purchase_orders.invitations',
'company.invoices.documents',
'company.products',
'company.products.documents',
'company.payments.paymentables',
'company.payments.documents',
'company.purchase_orders.documents',
'company.payment_terms.company',
'company.projects.documents',
'company.recurring_expenses',
'company.recurring_invoices',
'company.recurring_invoices.invitations.contact',
'company.recurring_invoices.invitations.company',
'company.recurring_invoices.documents',
'company.quotes.invitations.contact',
'company.quotes.invitations.company',
'company.quotes.documents',
'company.tasks.documents',
'company.subscriptions',
'company.tax_rates',
'company.tokens_hashed',
'company.vendors.contacts.company',
'company.vendors.documents',
'company.webhooks',
'company.system_logs',
];
private $mini_load = [
'account',
@ -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,8 +235,11 @@ 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;
if ($invoice->partial > 0) {

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();
$invoice = Invoice::with('client')->find($payment_hash->fee_invoice_id);
$client = $invoice ? $invoice->client : auth()->user()->client;
return $gateway
// ->driver(auth()->user()->client)
$gateway = CompanyGateway::findOrFail($request->input('company_gateway_id'));
$payment_hash = PaymentHash::where('hash', $request->payment_hash)->firstOrFail();
$invoice = Invoice::with('client')->find($payment_hash->fee_invoice_id);
$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($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,19 +272,25 @@ 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;
}
//if phantom js...... inject here..
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
//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 (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());

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')]);
}
});
@ -594,7 +597,7 @@ class PurchaseOrderController extends BaseController
}
private function performAction(PurchaseOrder $purchase_order, $action, $bulk = false)
{
{
/*If we are using bulk actions, we don't want to return anything */
switch ($action) {
case 'mark_sent':
@ -608,9 +611,9 @@ class PurchaseOrderController extends BaseController
$file = $purchase_order->service()->getPurchaseOrderPdf();
return response()->streamDownload(function () use ($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
break;
case 'restore':
@ -635,7 +638,7 @@ class PurchaseOrderController extends BaseController
return $this->listResponse($purchase_order);
}
break;
case 'email':
//check query parameter for email_type and set the template else use calculateTemplate
PurchaseOrderEmail::dispatch($purchase_order, $purchase_order->company);
@ -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,13 +210,13 @@ 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)));
$user->setCompany($company);
$user->company_id = $company->id;
return $this->itemResponse($user);
}
@ -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()) {
return response()->json(['message', 'Cannot detach owner.'], 400);
}
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,37 +45,35 @@ class InvitationController extends Controller
public function purchaseOrder(string $invitation_key)
{
Auth::logout();
$invitation = PurchaseOrderInvitation::withTrashed()
->where('key', $invitation_key)
->whereHas('purchase_order', function ($query) {
$query->where('is_deleted', 0);
$query->where('is_deleted',0);
})
->with('contact.vendor')
->first();
if (! $invitation) {
return abort(404, 'The resource is no longer available.');
}
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);
$file_name = $invitation->purchase_order->numberFormatter().'.pdf';
// return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf');
// }
// $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db);
// private function returnRawPdf(string $entity, string $invitation_key)
// {
$file = (new CreatePurchaseOrderPdf($invitation))->rawPdf();
// if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
// return response()->json(['message' => 'Invalid resource request']);
$headers = ['Content-Type' => 'application/pdf'];
// $key = $entity.'_id';
if(request()->input('inline') == 'true')
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
// $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
return response()->streamDownload(function () use($file) {
echo $file;
}, $file_name, $headers);
}
// $invitation = $entity_obj::where('key', $invitation_key)
// ->with('contact.client')
// ->firstOrFail();
// if(!$invitation)
// return response()->json(["message" => "no record found"], 400);
// $file_name = $invitation->purchase_order->numberFormatter().'.pdf';
// $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db);
// $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);
// }
}

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;
}
}
@ -237,7 +241,7 @@ class BillingPortalPurchase extends Component
$company = $this->subscription->company;
$user = $this->subscription->user;
$user->setCompany($company);
$client_repo = new ClientRepository(new ClientContactRepository());
$data = [
@ -267,7 +271,7 @@ class BillingPortalPurchase extends Component
})->first();
if ($record) {
$data['settings']['language_id'] = (string) $record->id;
$data['settings']['language_id'] = (string)$record->id;
}
}
@ -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,8 +396,9 @@ class BillingPortalPurchase extends Component
public function handlePaymentNotRequired()
{
$is_eligible = $this->subscription->service()->isEligible($this->contact);
$is_eligible = $this->subscription->service()->isEligible($this->contact);
if ($is_eligible['status_code'] != 200) {
$this->steps['not_eligible'] = true;
$this->steps['not_eligible_message'] = $is_eligible['message'];
@ -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->quantity--;
$this->price = $this->subscription->promo_price * $this->quantity;
return 0;
return $this->quantity;
}
public function handleCoupon()
@ -458,12 +463,12 @@ class BillingPortalPurchase extends Component
->first();
$mailer = new NinjaMailerObject();
$mailer->mailable = new ContactPasswordlessLogin($this->email, $this->subscription->company, (string) route('client.subscription.purchase', $this->subscription->hashed_id).'?coupon='.$this->coupon);
$mailer->mailable = new ContactPasswordlessLogin($this->email, $this->subscription->company, (string)route('client.subscription.purchase', $this->subscription->hashed_id) . '?coupon=' . $this->coupon);
$mailer->company = $this->subscription->company;
$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) {
$timeout = 30 * 60 * 1000 * 1000;
} else {
$timeout = $timeout / 1000;
}
if($timeout == 0)
$timeout = 30*60*1000*1000;
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) {
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) {
}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
$user = false;
$google = new Google();
$user = $google->getTokenResponse(request()->header('X-API-OAUTH-PASSWORD'));
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',
];
if (is_array($user)) {
$query = [
'oauth_user_id' => $google->harvestSubField($user),
'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');
//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");
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);
} 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);
}
}
return response()->json($error, 412);
} elseif ($x_api_password && Hash::check($x_api_password, auth()->user()->password)) {
}elseif ($x_api_password && Hash::check($x_api_password, auth()->user()->password)) {
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
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.
*
@ -42,20 +44,26 @@ class StorePurchaseOrderRequest extends Request
$rules['vendor_id'] = 'bail|required|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', auth()->user()->company()->id)];
$rules['discount'] = 'sometimes|numeric';
$rules['discount'] = 'sometimes|numeric';
$rules['is_amount_discount'] = ['boolean'];
$rules['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("trying to send to {$this->nmo->to_user->email} ". now()->toDateTimeString());
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();
@ -121,26 +126,25 @@ class NinjaMailerJob implements ShouldQueue
* this merges a text string with a json object
* need to harvest the ->Message property using the following
*/
if ($e instanceof ClientException) { //postmark specific failure
if($e instanceof ClientException) { //postmark specific failure
$response = $e->getResponse();
$message_body = json_decode($response->getBody()->getContents());
if ($message_body && property_exists($message_body, 'Message')) {
if($message_body && property_exists($message_body, 'Message')){
$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);
}
}
}
@ -157,13 +161,12 @@ 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()
@ -195,45 +199,65 @@ class NinjaMailerJob implements ShouldQueue
$sending_user = $this->nmo->settings->gmail_sending_user_id;
$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) {
$message->getHeaders()->addTextHeader('GmailToken', $token);
->withSwiftMessage(function ($message) use($token) {
$message->getHeaders()->addTextHeader('GmailToken', $token);
});
sleep(rand(1, 3));
sleep(rand(1,3));
}
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 {
try{
if ($google->getClient()->isAccessTokenExpired()) {
$google->refreshToken($user);
$user = $user->fresh();
@ -241,21 +265,21 @@ class NinjaMailerJob implements ShouldQueue
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
sleep(rand(2, 6));
} catch (\Exception $e) {
sleep(rand(2,6));
}
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) {
if(!$user->oauth_user_token) {
$this->company->account->gmailCredentialNotification();
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
@ -267,53 +291,58 @@ class NinjaMailerJob implements ShouldQueue
$token = $user->oauth_user_token->access_token;
if (! $token) {
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) {
$message->getHeaders()->addTextHeader('GmailToken', $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,27 +362,53 @@ class NinjaMailerJob implements ShouldQueue
public function failed($exception = null)
{
}
private function refreshOfficeToken($user)
{
$guzzle = new \GuzzleHttp\Client();
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
$expiry = $user->oauth_user_token_expiry ?: now()->subDay();
$token = json_decode($guzzle->post($url, [
'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',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token,
],
])->getBody()->getContents());
if($expiry->lt(now()))
{
$guzzle = new \GuzzleHttp\Client();
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
if ($token) {
return $token->access_token;
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id') ,
'client_secret' => config('ninja.o365.client_secret') ,
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => '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 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']);
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,32 +92,33 @@ class ProcessPostmarkWebhook implements ShouldQueue
case 'Open':
return $this->processOpen();
default:
// code...
# code...
break;
}
}
// {
// "Metadata": {
// {
// "Metadata": {
// "example": "value",
// "example_2": "value"
// },
// "RecordType": "Open",
// "FirstOpen": true,
// "Client": {
// },
// "RecordType": "Open",
// "FirstOpen": true,
// "Client": {
// "Name": "Chrome 35.0.1916.153",
// "Company": "Google",
// "Family": "Chrome"
// },
// "OS": {
// },
// "OS": {
// "Name": "OS X 10.7 Lion",
// "Company": "Apple Computer, Inc.",
// "Family": "OS X 10"
// },
// "Platform": "WebMail",
// "UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
// "ReadSeconds": 5,
// "Geo": {
// },
// "Platform": "WebMail",
// "UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
// "ReadSeconds": 5,
// "Geo": {
// "CountryISOCode": "RS",
// "Country": "Serbia",
// "RegionISOCode": "VO",
@ -127,81 +127,83 @@ class ProcessPostmarkWebhook implements ShouldQueue
// "Zip": "21000",
// "Coords": "45.2517,19.8369",
// "IP": "188.2.95.4"
// },
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "MessageStream": "outbound",
// "ReceivedAt": "2022-02-06T06:37:48Z",
// "Tag": "welcome-email",
// "Recipient": "john@example.com"
// }
// },
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "MessageStream": "outbound",
// "ReceivedAt": "2022-02-06T06:37:48Z",
// "Tag": "welcome-email",
// "Recipient": "john@example.com"
// }
private function processOpen()
{
$this->invitation->opened_date = now();
$this->invitation->save();
SystemLogger::dispatch($this->request,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_OPENED,
SystemLog::TYPE_WEBHOOK_RESPONSE,
SystemLogger::dispatch($this->request,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_OPENED,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->invitation->contact->client,
$this->invitation->company
);
}
// {
// "RecordType": "Delivery",
// "ServerID": 23,
// "MessageStream": "outbound",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "Recipient": "john@example.com",
// "Tag": "welcome-email",
// "DeliveredAt": "2021-02-21T16:34:52Z",
// "Details": "Test delivery webhook details",
// "Metadata": {
// {
// "RecordType": "Delivery",
// "ServerID": 23,
// "MessageStream": "outbound",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "Recipient": "john@example.com",
// "Tag": "welcome-email",
// "DeliveredAt": "2021-02-21T16:34:52Z",
// "Details": "Test delivery webhook details",
// "Metadata": {
// "example": "value",
// "example_2": "value"
// }
// }
// }
// }
private function processDelivery()
{
$this->invitation->email_status = 'delivered';
$this->invitation->save();
SystemLogger::dispatch($this->request,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_DELIVERY,
SystemLog::TYPE_WEBHOOK_RESPONSE,
SystemLogger::dispatch($this->request,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_DELIVERY,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->invitation->contact->client,
$this->invitation->company
);
}
// {
// "Metadata": {
// {
// "Metadata": {
// "example": "value",
// "example_2": "value"
// },
// "RecordType": "Bounce",
// "ID": 42,
// "Type": "HardBounce",
// "TypeCode": 1,
// "Name": "Hard bounce",
// "Tag": "Test",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "ServerID": 1234,
// "MessageStream": "outbound",
// "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
// "Details": "Test bounce details",
// "Email": "john@example.com",
// "From": "sender@example.com",
// "BouncedAt": "2021-02-21T16:34:52Z",
// "DumpAvailable": true,
// "Inactive": true,
// "CanActivate": true,
// "Subject": "Test subject",
// "Content": "Test content"
// }
// },
// "RecordType": "Bounce",
// "ID": 42,
// "Type": "HardBounce",
// "TypeCode": 1,
// "Name": "Hard bounce",
// "Tag": "Test",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "ServerID": 1234,
// "MessageStream": "outbound",
// "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
// "Details": "Test bounce details",
// "Email": "john@example.com",
// "From": "sender@example.com",
// "BouncedAt": "2021-02-21T16:34:52Z",
// "DumpAvailable": true,
// "Inactive": true,
// "CanActivate": true,
// "Subject": "Test subject",
// "Content": "Test content"
// }
private function processBounce()
{
@ -218,38 +220,39 @@ 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();
}
// {
// "Metadata": {
// {
// "Metadata": {
// "example": "value",
// "example_2": "value"
// },
// "RecordType": "SpamComplaint",
// "ID": 42,
// "Type": "SpamComplaint",
// "TypeCode": 100001,
// "Name": "Spam complaint",
// "Tag": "Test",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "ServerID": 1234,
// "MessageStream": "outbound",
// "Description": "The subscriber explicitly marked this message as spam.",
// "Details": "Test spam complaint details",
// "Email": "john@example.com",
// "From": "sender@example.com",
// "BouncedAt": "2021-02-21T16:34:52Z",
// "DumpAvailable": true,
// "Inactive": true,
// "CanActivate": false,
// "Subject": "Test subject",
// "Content": "Test content"
// }
// },
// "RecordType": "SpamComplaint",
// "ID": 42,
// "Type": "SpamComplaint",
// "TypeCode": 100001,
// "Name": "Spam complaint",
// "Tag": "Test",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "ServerID": 1234,
// "MessageStream": "outbound",
// "Description": "The subscriber explicitly marked this message as spam.",
// "Details": "Test spam complaint details",
// "Email": "john@example.com",
// "From": "sender@example.com",
// "BouncedAt": "2021-02-21T16:34:52Z",
// "DumpAvailable": true,
// "Inactive": true,
// "CanActivate": false,
// "Subject": "Test subject",
// "Content": "Test content"
// }
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();

File diff suppressed because it is too large Load Diff

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.
*
@ -74,7 +78,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue
{
$this->invitation = $invitation;
$this->company = $invitation->company;
$this->entity = $invitation->purchase_order;
$this->entity_string = 'purchase_order';
@ -82,12 +86,40 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$this->vendor = $invitation->contact->vendor;
$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*/
@ -106,27 +138,26 @@ 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 {
$template = new PdfMakerDesign(strtolower($design->name));
@ -160,23 +191,28 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$pdf = null;
try {
if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, $this->company);
if ($numbered_pdf) {
if($numbered_pdf)
$pdf = $numbered_pdf;
}
} else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
}
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 ?: '';
}
@ -310,15 +287,16 @@ class Account extends BaseModel
$trial_active = false;
//14 day trial
$duration = 60 * 60 * 24 * 14;
$duration = 60*60*24*14;
if ($trial_plan && $include_trial) {
$trial_started = $this->trial_started;
$trial_expires = Carbon::parse($this->trial_started)->addSeconds($duration);
if ($trial_expires->greaterThan(now())) {
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) {
return 20;
}
if($this->is_flagged)
return 0;
if ($this->isPaid()) {
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}"))) {
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}"))) {
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)) {
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,39 +53,46 @@ 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)) {
$payment = Payment::query()
foreach ($this->stripe_request as $transaction) {
if(array_key_exists('payment_intent', $transaction))
{
$payment = Payment::query()
->where('company_id', $company->id)
->where(function ($query) use ($transaction) {
$query->where('transaction_reference', $transaction['payment_intent'])
->orWhere('transaction_reference', $transaction['id']);
})
})
->first();
} else {
$payment = Payment::query()
}
else
{
$payment = Payment::query()
->where('company_id', $company->id)
->where('transaction_reference', $transaction['id'])
->first();
}
if ($payment) {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
$this->payment_completed = true;
}
}
if ($payment) {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
$this->payment_completed = true;
}
}
if ($this->payment_completed) {
if($this->payment_completed)
return;
}
if (optional($this->stripe_request['object']['charges']['data'][0])['id']) {
if(optional($this->stripe_request['object']['charges']['data'][0])['id']){
$company = Company::where('company_key', $this->company_key)->first();
$payment = Payment::query()
@ -93,29 +100,30 @@ class PaymentIntentWebhook implements ShouldQueue
->where('transaction_reference', $this->stripe_request['object']['charges']['data'][0]['id'])
->first();
//return early
if ($payment && $payment->status_id == Payment::STATUS_COMPLETED) {
nlog(' payment found and status correct - returning ');
//return early
if($payment && $payment->status_id == Payment::STATUS_COMPLETED){
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)
@ -145,7 +167,7 @@ class PaymentIntentWebhook implements ShouldQueue
'transaction_reference' => $this->stripe_request['object']['charges']['data'][0]['id'],
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
$payment = $driver->createPayment($data, Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
@ -156,5 +178,7 @@ class PaymentIntentWebhook implements ShouldQueue
$client,
$client->company,
);
}
}
}

View File

@ -29,10 +29,9 @@ class BaseRepository
use MakesHash;
use SavesDocuments;
public bool $import_mode = false;
public bool $import_mode = false;
private bool $new_model = false;
/**
* @param $entity
* @param $type
@ -138,10 +137,9 @@ 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);
$invitation = $invitation_class::where('key', $invitation['key'])->first();
@ -153,20 +151,20 @@ 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:
return 'quote_id';
case $model instanceof Credit:
return 'credit_id';
case ($model instanceof Quote):
return 'quote_id';
case ($model instanceof Credit):
return 'credit_id';
}
}
/**
* Alternative save used for Invoices, Recurring Invoices, Quotes & Credits.
*
*
* @param $data
* @param $model
* @return mixed
@ -175,11 +173,10 @@ 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();
$client = Client::where('id', $model->client_id)->withTrashed()->firstOrFail();
$state = [];
@ -198,14 +195,12 @@ 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);
$model->custom_surcharge_tax1 = $client->company->custom_surcharge_taxes1;
@ -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,81 +292,82 @@ 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) {
event('eloquent.created: App\Models\Credit', $model);
} else {
if($this->new_model)
event('eloquent.created: App\Models\Credit', $model);
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) {
$model->design_id = $this->decodePrimaryKey($client->getSetting('invoice_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,22 +30,20 @@ use Illuminate\Support\Carbon;
/**
* PaymentRepository.
*/
class PaymentRepository extends BaseRepository
{
use MakesHash;
use SavesDocuments;
class PaymentRepository extends BaseRepository {
use MakesHash;
use SavesDocuments;
protected $credit_repo;
protected $credit_repo;
public function __construct(CreditRepository $credit_repo)
{
$this->credit_repo = $credit_repo;
}
public function __construct( CreditRepository $credit_repo ) {
$this->credit_repo = $credit_repo;
}
/**
* Saves and updates a payment. //todo refactor to handle refunds and payments.
*
* @param array $data the request object
/**
* Saves and updates a payment. //todo refactor to handle refunds and payments.
*
* @param array $data the request object
* @param Payment $payment The Payment object
* @return Payment|null Payment $payment
*/
@ -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,8 +83,8 @@ class PaymentRepository extends BaseRepository
$client->service()->updatePaidToDate($data['amount'])->save();
}
// elseif($data['amount'] >0){
else {
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,20 +166,20 @@ 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') {
$payment->service()->sendEmail();
} elseif (! array_key_exists('email_receipt', $data) && $payment->client->getSetting('client_manual_payment_notification')) {
$payment->service()->sendEmail();
}
if ( ! $is_existing_payment && ! $this->import_mode ) {
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
}
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'))
$payment->service()->sendEmail();
event( new PaymentWasCreated( $payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null) ) );
}
$payment->applied += ($invoice_totals - $credit_totals); //wont work because - check tests
@ -205,7 +207,8 @@ class PaymentRepository extends BaseRepository
*/
public function processExchangeRates($data, $payment)
{
if (array_key_exists('exchange_rate', $data) && isset($data['exchange_rate']) && $data['exchange_rate'] != 1) {
if(array_key_exists('exchange_rate', $data) && isset($data['exchange_rate']) && $data['exchange_rate'] != 1){
return $payment;
}
@ -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));
@ -223,7 +227,7 @@ class PaymentRepository extends BaseRepository
return $payment;
}
$payment->currency_id = $company_currency;
return $payment;

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 {
$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;
@ -40,23 +41,20 @@ class UpdateInvoicePayment
$paid_invoices = $this->payment_hash->invoices();
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($paid_invoices, 'invoice_id')))->withTrashed()->get();
$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!
@ -79,14 +82,14 @@ class UpdateInvoicePayment
$invoice->paid_to_date += $paid_amount;
$invoice->save();
$invoice = $invoice->service()
$invoice = $invoice->service()
->clearPartial()
// ->updateBalance($paid_amount * -1)
// ->updatePaidToDate($paid_amount)
->updateStatus()
->touchPdf()
->save();
$invoice->service()
->workFlow()
->save();
@ -115,14 +118,18 @@ class UpdateInvoicePayment
];
TransactionLog::dispatch(TransactionEvent::GATEWAY_PAYMENT_MADE, $transaction, $invoice->company->db);
});
});
/* Remove the event updater from within the loop to prevent race conditions */
$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
@ -107,7 +100,7 @@ class Design extends BaseDesign
$path = $this->options['custom_path'] ?? config('ninja.designs.base_path');
return file_get_contents(
$path.$this->design
$path . $this->design
);
}
@ -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'];
@ -192,7 +203,7 @@ class Design extends BaseDesign
$elements = [];
foreach ($variables as $variable) {
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-'.substr($variable, 1)]];
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-' . substr($variable, 1)]];
}
return $elements;
@ -205,7 +216,7 @@ class Design extends BaseDesign
$elements = [];
foreach ($variables as $variable) {
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_address-'.substr($variable, 1)]];
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_address-' . substr($variable, 1)]];
}
return $elements;
@ -215,14 +226,13 @@ class Design extends BaseDesign
{
$elements = [];
if (! $this->vendor) {
if(!$this->vendor)
return $elements;
}
$variables = $this->context['pdf_variables']['vendor_details'];
foreach ($variables as $variable) {
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-'.substr($variable, 1)]];
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]];
}
return $elements;
@ -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,10 +256,10 @@ 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'])) {
if (!is_null($this->context['contact'])) {
$elements[] = ['element' => 'p', 'content' => $this->context['contact']->email, 'show_empty' => false, 'properties' => ['data-ref' => 'delivery_note-contact.email']];
}
@ -260,7 +269,7 @@ class Design extends BaseDesign
$variables = $this->context['pdf_variables']['client_details'];
foreach ($variables as $variable) {
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'client_details-'.substr($variable, 1)]];
$elements[] = ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'client_details-' . substr($variable, 1)]];
}
return $elements;
@ -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());
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')],
@ -291,7 +303,7 @@ class Design extends BaseDesign
if ($this->entity instanceof Quote) {
$variables = $this->context['pdf_variables']['quote_details'];
if ($this->entity->partial > 0) {
$variables[] = '$quote.balance_due';
}
@ -301,8 +313,10 @@ class Design extends BaseDesign
$variables = $this->context['pdf_variables']['credit_details'];
}
if ($this->vendor) {
if($this->vendor){
$variables = $this->context['pdf_variables']['purchase_order_details'];
}
$elements = [];
@ -310,7 +324,7 @@ class Design extends BaseDesign
// We don't want to show account balance or invoice total on PDF.. or any amount with currency.
if ($this->type == self::DELIVERY_NOTE) {
$variables = array_filter($variables, function ($m) {
return ! in_array($m, ['$invoice.balance_due', '$invoice.total']);
return !in_array($m, ['$invoice.balance_due', '$invoice.total']);
});
}
@ -318,15 +332,18 @@ 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)]],
['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)]],
]];
} else {
$elements[] = ['element' => 'tr', 'properties' => ['hidden' => $this->entityVariableCheck($variable)], '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)]],
['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)]],
]];
}
}
@ -353,7 +370,7 @@ class Design extends BaseDesign
foreach ($items as $row) {
for ($i = 0; $i < count($product_customs); $i++) {
if (! empty($row['delivery_note.delivery_note'.($i + 1)])) {
if (!empty($row['delivery_note.delivery_note' . ($i + 1)])) {
$product_customs[$i] = true;
}
}
@ -361,7 +378,7 @@ class Design extends BaseDesign
for ($i = 0; $i < count($product_customs); $i++) {
if ($product_customs[$i]) {
array_push($thead, ['element' => 'th', 'content' => '$product.product'.($i + 1).'_label', 'properties' => ['data-ref' => 'delivery_note-product.product'.($i + 1).'_label']]);
array_push($thead, ['element' => 'th', 'content' => '$product.product' . ($i + 1) . '_label', 'properties' => ['data-ref' => 'delivery_note-product.product' . ($i + 1) . '_label']]);
}
}
@ -461,7 +478,7 @@ class Design extends BaseDesign
$outstanding = $this->invoices->sum('balance');
return [
['element' => 'p', 'content' => '$outstanding_label: '.Number::formatMoney($outstanding, $this->client)],
['element' => 'p', 'content' => '$outstanding_label: ' . Number::formatMoney($outstanding, $this->client)],
];
}
@ -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;
}
}
@ -521,14 +540,14 @@ class Design extends BaseDesign
public function statementPaymentTableTotals(): array
{
if (is_null($this->payments) || ! $this->payments->first() || $this->type !== self::STATEMENT) {
if (is_null($this->payments) || !$this->payments->first() || $this->type !== self::STATEMENT) {
return [];
}
if (\array_key_exists('show_payments_table', $this->options) && $this->options['show_payments_table'] === false) {
return [];
}
$payment = $this->payments->first();
return [
@ -583,19 +602,19 @@ class Design extends BaseDesign
foreach ($this->context['pdf_variables']["{$type}_columns"] as $column) {
if (array_key_exists($column, $aliases)) {
$elements[] = ['element' => 'th', 'content' => $aliases[$column].'_label', 'properties' => ['data-ref' => "{$type}_table-".substr($aliases[$column], 1).'-th', 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.discount' && ! $this->company->enable_product_discount) {
$elements[] = ['element' => 'th', 'content' => $column.'_label', 'properties' => ['data-ref' => "{$type}_table-".substr($column, 1).'-th', 'style' => 'display: none;']];
} elseif ($column == '$product.quantity' && ! $this->company->enable_product_quantity) {
$elements[] = ['element' => 'th', 'content' => $column.'_label', 'properties' => ['data-ref' => "{$type}_table-".substr($column, 1).'-th', 'style' => 'display: none;']];
$elements[] = ['element' => 'th', 'content' => $aliases[$column] . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($aliases[$column], 1) . '-th', 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.discount' && !$this->company->enable_product_discount) {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']];
} elseif ($column == '$product.quantity' && !$this->company->enable_product_quantity) {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']];
} elseif ($column == '$product.tax_rate1') {
$elements[] = ['element' => 'th', 'content' => $column.'_label', 'properties' => ['data-ref' => "{$type}_table-product.tax1-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax1-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.tax_rate2') {
$elements[] = ['element' => 'th', 'content' => $column.'_label', 'properties' => ['data-ref' => "{$type}_table-product.tax2-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax2-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.tax_rate3') {
$elements[] = ['element' => 'th', 'content' => $column.'_label', 'properties' => ['data-ref' => "{$type}_table-product.tax3-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax3-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} else {
$elements[] = ['element' => 'th', 'content' => $column.'_label', 'properties' => ['data-ref' => "{$type}_table-".substr($column, 1).'-th', 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
}
}
@ -625,7 +644,7 @@ class Design extends BaseDesign
foreach ($items as $row) {
for ($i = 0; $i < count($product_customs); $i++) {
if (! empty($row['delivery_note.delivery_note'.($i + 1)])) {
if (!empty($row['delivery_note.delivery_note' . ($i + 1)])) {
$product_customs[$i] = true;
}
}
@ -640,7 +659,7 @@ class Design extends BaseDesign
for ($i = 0; $i < count($product_customs); $i++) {
if ($product_customs[$i]) {
$element['elements'][] = ['element' => 'td', 'content' => $row['delivery_note.delivery_note'.($i + 1)], 'properties' => ['data-ref' => 'delivery_note_table.product'.($i + 1).'-td']];
$element['elements'][] = ['element' => 'td', 'content' => $row['delivery_note.delivery_note' . ($i + 1)], 'properties' => ['data-ref' => 'delivery_note_table.product' . ($i + 1) . '-td']];
}
}
@ -655,8 +674,8 @@ class Design extends BaseDesign
if (
array_key_exists($type, $this->context) &&
! empty($this->context[$type]) &&
! is_null($this->context[$type])
!empty($this->context[$type]) &&
!is_null($this->context[$type])
) {
$document = new DOMDocument();
$document->loadHTML($this->context[$type], LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
@ -686,9 +705,9 @@ class Design extends BaseDesign
if ($cell == '$task.rate') {
$element['elements'][] = ['element' => 'td', 'content' => $row['$task.cost'], 'properties' => ['data-ref' => 'task_table-task.cost-td']];
} elseif ($cell == '$product.discount' && ! $this->company->enable_product_discount) {
} elseif ($cell == '$product.discount' && !$this->company->enable_product_discount) {
$element['elements'][] = ['element' => 'td', 'content' => $row['$product.discount'], 'properties' => ['data-ref' => 'product_table-product.discount-td', 'style' => 'display: none;']];
} elseif ($cell == '$product.quantity' && ! $this->company->enable_product_quantity) {
} elseif ($cell == '$product.quantity' && !$this->company->enable_product_quantity) {
$element['elements'][] = ['element' => 'td', 'content' => $row['$product.quantity'], 'properties' => ['data-ref' => 'product_table-product.quantity-td', 'style' => 'display: none;']];
} elseif ($cell == '$task.hours') {
$element['elements'][] = ['element' => 'td', 'content' => $row['$task.quantity'], 'properties' => ['data-ref' => 'task_table-task.hours-td']];
@ -698,10 +717,10 @@ class Design extends BaseDesign
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax2-td']];
} elseif ($cell == '$product.tax_rate3') {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax3-td']];
} elseif ($cell == '$product.unit_cost' || $cell == '$task.rate') {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['style' => 'white-space: nowrap;', 'data-ref' => "{$_type}_table-".substr($cell, 1).'-td']];
} else if ($cell == '$product.unit_cost' || $cell == '$task.rate') {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['style' => 'white-space: nowrap;', 'data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']];
} else {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => "{$_type}_table-".substr($cell, 1).'-td']];
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']];
}
}
}
@ -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,12 +781,20 @@ 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);
if (
! is_null($this->entity->{$property}) &&
! empty($this->entity->{$property}) &&
!is_null($this->entity->{$property}) &&
!empty($this->entity->{$property}) &&
$this->entity->{$property} != 0
) {
continue;
@ -780,56 +809,56 @@ class Design extends BaseDesign
if ($variable == '$total_taxes') {
$taxes = $this->entity->calc()->getTotalTaxMap();
if (! $taxes) {
if (!$taxes) {
continue;
}
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' => $tax['name'], 'properties' => ['data-ref' => 'totals-table-total_tax_' . $i . '-label']],
['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') {
$taxes = $this->entity->calc()->getTaxMap();
if (! $taxes) {
if (!$taxes) {
continue;
}
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' => $tax['name'], 'properties' => ['data-ref' => 'totals-table-line_tax_' . $i . '-label']],
['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']],
['element' => 'span', 'content' => $variable, 'properties' => ['hidden' => ! $visible, 'data-ref' => 'totals_table-'.substr($variable, 1)]],
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],
['element' => 'span', 'content' => $variable, 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1)]],
]];
} elseif (Str::startsWith($variable, '$custom')) {
$field = explode('_', $variable);
$visible = is_object($this->company->custom_fields) && property_exists($this->company->custom_fields, $field[1]) && ! empty($this->company->custom_fields->{$field[1]});
$visible = is_object($this->company->custom_fields) && property_exists($this->company->custom_fields, $field[1]) && !empty($this->company->custom_fields->{$field[1]});
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => $variable.'_label', 'properties' => ['hidden' => ! $visible, 'data-ref' => 'totals_table-'.substr($variable, 1).'-label']],
['element' => 'span', 'content' => $variable, 'properties' => ['hidden' => ! $visible, 'data-ref' => 'totals_table-'.substr($variable, 1)]],
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],
['element' => 'span', 'content' => $variable, 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1)]],
]];
} else {
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => $variable.'_label', 'properties' => ['data-ref' => 'totals_table-'.substr($variable, 1).'-label']],
['element' => 'span', 'content' => $variable, 'properties' => ['data-ref' => 'totals_table-'.substr($variable, 1)]],
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],
['element' => 'span', 'content' => $variable, 'properties' => ['data-ref' => 'totals_table-' . substr($variable, 1)]],
]];
}
}
$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;
}
$this->trySaving();
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->invoice->footer) || empty($this->invoice->footer))
$this->purchase_order->footer = $settings->purchase_order_footer;
// if (!isset($this->purchase_order->public_notes))
// $this->purchase_order->public_notes = $this->purchase_order->client->public_notes;
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) {
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;
@ -26,7 +27,7 @@ class RecurringService
}
//set schedules - update next_send_dates
/**
* Stops a recurring invoice
*
@ -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,12 +50,13 @@ class RecurringService
public function start()
{
if ($this->recurring_entity->remaining_cycles == 0) {
return $this;
}
$this->setStatus(RecurringInvoice::STATUS_ACTIVE);
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');
$this->recurring_entity->invitations->each(function ($invitation){
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();
}
@ -100,8 +106,13 @@ class RecurringService
if ($request->has('stop') && $request->input('stop') == 'true') {
$this->stop();
}
if ($request->has('send_now') && $request->input('send_now') == 'true' && $this->recurring_entity->invoices()->count() == 0) {
$this->sendNow();
}
if (isset($this->recurring_entity->client)) {
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,11 +120,24 @@ class RecurringService
return $this;
}
public function fillDefaults()
public function sendNow()
{
return $this;
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;
}
public function save()
{
$this->recurring_entity->saveQuietly();

View File

@ -67,22 +67,24 @@ 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');
if (!property_exists($payment_hash->data, 'billing_context')) {
throw new \Exception("Illegal entrypoint into method, payload must contain billing context");
}
if ($payment_hash->data->billing_context->context == 'change_plan') {
if($payment_hash->data->billing_context->context == 'change_plan') {
return $this->handlePlanChange($payment_hash);
}
// if we have a recurring product - then generate a recurring invoice
if (strlen($this->subscription->recurring_product_ids) >= 1) {
if(strlen($this->subscription->recurring_product_ids) >=1){
$recurring_invoice = $this->convertInvoiceToRecurring($payment_hash->payment->client_id);
$recurring_invoice_repo = new RecurringInvoiceRepository();
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->auto_bill = $this->subscription->auto_bill;
/* Start the recurring service */
$recurring_invoice->service()
->start()
@ -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);
@ -178,13 +185,13 @@ class SubscriptionService
->start()
->save();
$context = [
'context' => 'trial',
'recurring_invoice' => $recurring_invoice->hashed_id,
'client' => $recurring_invoice->client->hashed_id,
'subscription' => $this->subscription->hashed_id,
'account_key' => $recurring_invoice->client->custom_value2,
];
$context = [
'context' => 'trial',
'recurring_invoice' => $recurring_invoice->hashed_id,
'client' => $recurring_invoice->client->hashed_id,
'subscription' => $this->subscription->hashed_id,
'account_key' => $recurring_invoice->client->custom_value2,
];
//execute any webhooks
$response = $this->triggerWebhook($context);
@ -223,9 +230,10 @@ class SubscriptionService
->orderBy('id', 'desc')
->first();
//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)
//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)
->orderBy('id', 'desc')
@ -234,21 +242,25 @@ class SubscriptionService
//need to ensure at this point that a refund is appropriate!!
//28-02-2022
if ($recurring_invoice->invoices()->count() == 0) {
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);
@ -271,14 +282,15 @@ class SubscriptionService
$days_in_frequency = $this->getDaysInFrequency();
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used) / $days_in_frequency) * $this->subscription->price, 2);
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $this->subscription->price ,2);
// nlog("days in frequency = {$days_in_frequency} - days of subscription used {$days_of_subscription_used}");
// nlog("invoice amount = {$invoice->amount}");
// nlog("pro rata refund = {$pro_rata_refund}");
return $pro_rata_refund;
}
}
/**
* We refund unused days left.
@ -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,17 +311,17 @@ 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);
$pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2);
// nlog("days in frequency = {$days_in_frequency} - days of subscription used {$days_of_subscription_used}");
// nlog("invoice amount = {$invoice->amount}");
// nlog("pro rata refund = {$pro_rata_refund}");
return $pro_rata_refund;
}
/**
@ -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;
@ -339,21 +349,28 @@ class SubscriptionService
// $days_in_frequency = $this->getDaysInFrequency();
$days_in_frequency = $invoice->subscription->service()->getDaysInFrequency();
$ratio = ($days_in_frequency - $days_of_subscription_used) / $days_in_frequency;
$ratio = ($days_in_frequency - $days_of_subscription_used)/$days_in_frequency;
$line_items = [];
foreach ($invoice->line_items as $item) {
if ($item->product_key != ctrans('texts.refund')) {
$item->cost = ($item->cost * $ratio * $multiplier);
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();
@ -374,7 +392,7 @@ class SubscriptionService
nlog("days to charge = {$days_to_charge} days in frequency = {$days_in_frequency}");
$pro_rata_charge = round(($days_to_charge / $days_in_frequency) * $invoice->amount, 2);
$pro_rata_charge = round(($days_to_charge/$days_in_frequency) * $invoice->amount ,2);
nlog("pro rata charge = {$pro_rata_charge}");
@ -404,9 +422,11 @@ class SubscriptionService
->orderBy('id', 'desc')
->first();
if ($recurring_invoice->invoices()->count() == 0) {
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)
@ -414,13 +434,19 @@ class SubscriptionService
->where('is_deleted', 0)
->withTrashed()
->orderBy('id', 'desc')
->first();
->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,35 +458,35 @@ 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);
$context = [
'context' => 'change_plan',
'recurring_invoice' => $new_recurring_invoice->hashed_id,
'credit' => $credit ? $credit->hashed_id : null,
'client' => $new_recurring_invoice->client->hashed_id,
'subscription' => $target_subscription->hashed_id,
'contact' => auth()->guard('contact')->user()->hashed_id,
'account_key' => $new_recurring_invoice->client->custom_value2,
];
$context = [
'context' => 'change_plan',
'recurring_invoice' => $new_recurring_invoice->hashed_id,
'credit' => $credit ? $credit->hashed_id : null,
'client' => $new_recurring_invoice->client->hashed_id,
'subscription' => $target_subscription->hashed_id,
'contact' => auth()->guard('contact')->user()->hashed_id,
'account_key' => $new_recurring_invoice->client->custom_value2,
];
$response = $this->triggerWebhook($context);
$response = $this->triggerWebhook($context);
nlog($response);
nlog($response);
if($credit)
return $this->handleRedirect('/client/credits/'.$credit->hashed_id);
else
return $this->handleRedirect('/client/credits');
if ($credit) {
return $this->handleRedirect('/client/credits/'.$credit->hashed_id);
} 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'];
@ -517,12 +547,16 @@ class SubscriptionService
->orderBy('id', 'desc')
->first();
if (! $last_invoice) {
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,24 +612,26 @@ class SubscriptionService
*/
public function createNewRecurringInvoice($old_recurring_invoice) :RecurringInvoice
{
$old_recurring_invoice->service()->stop()->save();
$recurring_invoice_repo = new RecurringInvoiceRepository();
$recurring_invoice_repo->delete($old_recurring_invoice);
$recurring_invoice = $this->convertInvoiceToRecurring($old_recurring_invoice->client_id);
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->next_send_date = now()->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
$recurring_invoice = $this->convertInvoiceToRecurring($old_recurring_invoice->client_id);
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->next_send_date = now()->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
/* Start the recurring service */
$recurring_invoice->service()
/* Start the recurring service */
$recurring_invoice->service()
->start()
->save();
return $recurring_invoice;
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);
}
/**
@ -695,7 +742,7 @@ class SubscriptionService
public function convertInvoiceToRecurring($client_id) :RecurringInvoice
{
MultiDB::setDb($this->subscription->company->db);
$client = Client::withTrashed()->find($client_id);
$subscription_repo = new SubscriptionRepository();
@ -708,13 +755,12 @@ class SubscriptionService
$recurring_invoice->date = now();
$recurring_invoice->remaining_cycles = -1;
$recurring_invoice->auto_bill = $client->getSetting('auto_bill');
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->due_date_days = 'terms';
$recurring_invoice->next_send_date = now()->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date = $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,18 +797,23 @@ 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)) {
if(is_array($response)){
$body = $response;
} else {
}
else {
$body = $response->getStatusCode();
}
$client = Client::where('id', $this->decodePrimaryKey($body['client']))->withTrashed()->first();
SystemLogger::dispatch(
SystemLogger::dispatch(
$body,
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_RESPONSE,
@ -769,14 +821,14 @@ class SubscriptionService
$client,
$client->company,
);
nlog("ready to fire back");
nlog('ready to fire back');
if(is_array($body))
return $response;
else
return ['message' => 'There was a problem encountered with the webhook', 'status_code' => 500];
if (is_array($body)) {
return $response;
} else {
return ['message' => 'There was a problem encountered with the webhook', 'status_code' => 500];
}
}
public function fireNotifications()
@ -792,17 +844,15 @@ 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();
}
}
/**
@ -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)) {
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 = [
@ -882,39 +937,47 @@ class SubscriptionService
}
}
$context = [
'context' => 'cancellation',
'subscription' => $this->subscription->hashed_id,
'recurring_invoice' => $recurring_invoice->hashed_id,
'client' => $recurring_invoice->client->hashed_id,
'contact' => auth()->guard('contact')->user()->hashed_id,
'account_key' => $recurring_invoice->client->custom_value2,
];
$context = [
'context' => 'cancellation',
'subscription' => $this->subscription->hashed_id,
'recurring_invoice' => $recurring_invoice->hashed_id,
'client' => $recurring_invoice->client->hashed_id,
'contact' => auth()->guard('contact')->user()->hashed_id,
'account_key' => $recurring_invoice->client->custom_value2,
];
$this->triggerWebhook($context);
$this->triggerWebhook($context);
$nmo = new NinjaMailerObject;
$nmo->mailable = (new NinjaMailer((new ClientContactRequestCancellationObject($recurring_invoice, auth()->guard('contact')->user()))->build()));
$nmo->company = $recurring_invoice->company;
$nmo->settings = $recurring_invoice->company->settings;
$nmo = new NinjaMailerObject;
$nmo->mailable = (new NinjaMailer((new ClientContactRequestCancellationObject($recurring_invoice, auth()->guard('contact')->user()))->build()));
$nmo->company = $recurring_invoice->company;
$nmo->settings = $recurring_invoice->company->settings;
$recurring_invoice->company->company_users->each(function ($company_user) use ($nmo){
$recurring_invoice->company->company_users->each(function ($company_user) use ($nmo) {
$methods = $this->findCompanyUserNotificationType($company_user, ['recurring_cancellation', 'all_notifications']);
$methods = $this->findCompanyUserNotificationType($company_user, ['recurring_cancellation', 'all_notifications']);
//if mail is a method type -fire mail!!
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
//if mail is a method type -fire mail!!
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
$nmo->to_user = $company_user->user;
NinjaMailerJob::dispatch($nmo);
}
});
$nmo->to_user = $company_user->user;
NinjaMailerJob::dispatch($nmo);
}
});
return $this->handleRedirect('client/subscriptions');
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)
@ -974,25 +1038,27 @@ class SubscriptionService
return $date->addYears(3);
default:
return 0;
}
}
}
/**
* 'email' => $this->email ?? $this->contact->email,
* 'quantity' => $this->quantity,
* 'contact_id' => $this->contact->id,
*/
* 'email' => $this->email ?? $this->contact->email,
* 'quantity' => $this->quantity,
* 'contact_id' => $this->contact->id,
*/
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,36 +1066,36 @@ 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);
}
/**
* @param Invoice $invoice
* @return true
* @throws BindingResolutionException
* @param Invoice $invoice
* @return true
* @throws BindingResolutionException
*/
public function planPaid(Invoice $invoice)
{
$recurring_invoice_hashed_id = $invoice->recurring_invoice()->exists() ? $invoice->recurring_invoice->hashed_id : null;
$context = [
'context' => 'plan_paid',
'subscription' => $this->subscription->hashed_id,
'recurring_invoice' => $recurring_invoice_hashed_id,
'client' => $invoice->client->hashed_id,
'contact' => $invoice->client->primary_contact()->first() ? $invoice->client->primary_contact()->first()->hashed_id : $invoice->client->contacts->first()->hashed_id,
'invoice' => $invoice->hashed_id,
'account_key' => $invoice->client->custom_value2,
];
$context = [
'context' => 'plan_paid',
'subscription' => $this->subscription->hashed_id,
'recurring_invoice' => $recurring_invoice_hashed_id,
'client' => $invoice->client->hashed_id,
'contact' => $invoice->client->primary_contact()->first() ? $invoice->client->primary_contact()->first()->hashed_id: $invoice->client->contacts->first()->hashed_id,
'invoice' => $invoice->hashed_id,
'account_key' => $invoice->client->custom_value2,
];
$response = $this->triggerWebhook($context);
nlog($response);
return true;
}
}

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

@ -23,7 +23,7 @@ class ExpenseTransformer extends EntityTransformer
{
use MakesHash;
use SoftDeletes;
protected $defaultIncludes = [
'documents',
];
@ -78,8 +78,7 @@ 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 ?: '',
'number' => (string)$expense->number ?: '',
'payment_date' => $expense->payment_date ?: '',
'custom_value1' => $expense->custom_value1 ?: '',
'custom_value2' => $expense->custom_value2 ?: '',

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 [
@ -37,18 +59,18 @@ class PurchaseOrderTransformer extends EntityTransformer
'user_id' => $this->encodePrimaryKey($purchase_order->user_id),
'project_id' => $this->encodePrimaryKey($purchase_order->project_id),
'assigned_user_id' => $this->encodePrimaryKey($purchase_order->assigned_user_id),
'vendor_id' => (string) $this->encodePrimaryKey($purchase_order->vendor_id),
'amount' => (float) $purchase_order->amount,
'balance' => (float) $purchase_order->balance,
'client_id' => (string) $this->encodePrimaryKey($purchase_order->client_id),
'status_id' => (string) ($purchase_order->status_id ?: 1),
'design_id' => (string) $this->encodePrimaryKey($purchase_order->design_id),
'created_at' => (int) $purchase_order->created_at,
'updated_at' => (int) $purchase_order->updated_at,
'archived_at' => (int) $purchase_order->deleted_at,
'is_deleted' => (bool) $purchase_order->is_deleted,
'vendor_id' => (string)$this->encodePrimaryKey($purchase_order->vendor_id),
'amount' => (float)$purchase_order->amount,
'balance' => (float)$purchase_order->balance,
'client_id' => (string)$this->encodePrimaryKey($purchase_order->client_id),
'status_id' => (string)($purchase_order->status_id ?: 1),
'design_id' => (string)$this->encodePrimaryKey($purchase_order->design_id),
'created_at' => (int)$purchase_order->created_at,
'updated_at' => (int)$purchase_order->updated_at,
'archived_at' => (int)$purchase_order->deleted_at,
'is_deleted' => (bool)$purchase_order->is_deleted,
'number' => $purchase_order->number ?: '',
'discount' => (float) $purchase_order->discount,
'discount' => (float)$purchase_order->discount,
'po_number' => $purchase_order->po_number ?: '',
'date' => $purchase_order->date ?: '',
'last_sent_date' => $purchase_order->last_sent_date ?: '',
@ -61,37 +83,39 @@ class PurchaseOrderTransformer extends EntityTransformer
'terms' => $purchase_order->terms ?: '',
'public_notes' => $purchase_order->public_notes ?: '',
'private_notes' => $purchase_order->private_notes ?: '',
'uses_inclusive_taxes' => (bool) $purchase_order->uses_inclusive_taxes,
'uses_inclusive_taxes' => (bool)$purchase_order->uses_inclusive_taxes,
'tax_name1' => $purchase_order->tax_name1 ? $purchase_order->tax_name1 : '',
'tax_rate1' => (float) $purchase_order->tax_rate1,
'tax_rate1' => (float)$purchase_order->tax_rate1,
'tax_name2' => $purchase_order->tax_name2 ? $purchase_order->tax_name2 : '',
'tax_rate2' => (float) $purchase_order->tax_rate2,
'tax_rate2' => (float)$purchase_order->tax_rate2,
'tax_name3' => $purchase_order->tax_name3 ? $purchase_order->tax_name3 : '',
'tax_rate3' => (float) $purchase_order->tax_rate3,
'total_taxes' => (float) $purchase_order->total_taxes,
'is_amount_discount' => (bool) ($purchase_order->is_amount_discount ?: false),
'tax_rate3' => (float)$purchase_order->tax_rate3,
'total_taxes' => (float)$purchase_order->total_taxes,
'is_amount_discount' => (bool)($purchase_order->is_amount_discount ?: false),
'footer' => $purchase_order->footer ?: '',
'partial' => (float) ($purchase_order->partial ?: 0.0),
'partial' => (float)($purchase_order->partial ?: 0.0),
'partial_due_date' => $purchase_order->partial_due_date ?: '',
'custom_value1' => (string) $purchase_order->custom_value1 ?: '',
'custom_value2' => (string) $purchase_order->custom_value2 ?: '',
'custom_value3' => (string) $purchase_order->custom_value3 ?: '',
'custom_value4' => (string) $purchase_order->custom_value4 ?: '',
'has_tasks' => (bool) $purchase_order->has_tasks,
'has_expenses' => (bool) $purchase_order->has_expenses,
'custom_surcharge1' => (float) $purchase_order->custom_surcharge1,
'custom_surcharge2' => (float) $purchase_order->custom_surcharge2,
'custom_surcharge3' => (float) $purchase_order->custom_surcharge3,
'custom_surcharge4' => (float) $purchase_order->custom_surcharge4,
'custom_surcharge_tax1' => (bool) $purchase_order->custom_surcharge_tax1,
'custom_surcharge_tax2' => (bool) $purchase_order->custom_surcharge_tax2,
'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',
'exchange_rate' => (float) $purchase_order->exchange_rate,
'paid_to_date' => (float) $purchase_order->paid_to_date,
'custom_value1' => (string)$purchase_order->custom_value1 ?: '',
'custom_value2' => (string)$purchase_order->custom_value2 ?: '',
'custom_value3' => (string)$purchase_order->custom_value3 ?: '',
'custom_value4' => (string)$purchase_order->custom_value4 ?: '',
'has_tasks' => (bool)$purchase_order->has_tasks,
'has_expenses' => (bool)$purchase_order->has_expenses,
'custom_surcharge1' => (float)$purchase_order->custom_surcharge1,
'custom_surcharge2' => (float)$purchase_order->custom_surcharge2,
'custom_surcharge3' => (float)$purchase_order->custom_surcharge3,
'custom_surcharge4' => (float)$purchase_order->custom_surcharge4,
'custom_surcharge_tax1' => (bool)$purchase_order->custom_surcharge_tax1,
'custom_surcharge_tax2' => (bool)$purchase_order->custom_surcharge_tax2,
'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' => '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)
{
$this->labels_and_values = (new HtmlEngine($this->entity_obj->invitations->first()))->generateLabelsAndValues();
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();
$data = array_merge($data, Helpers::sharedEmailVariables($this->entity_obj->client));
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,12 +319,60 @@ class TemplateEngine
]);
}
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('client', $client);
$this->entity_obj->setRelation('company', auth()->user()->company());
$this->entity_obj->load('client');
$client->setRelation('company', auth()->user()->company());
$client->load('company');
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());
$this->entity_obj->load('client');
$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

@ -35,9 +35,9 @@ trait SettingsSaver
ksort($casts);
foreach ($casts as $key => $value) {
//try casting floats here
if ($value == 'float' && property_exists($settings, $key)) {
if($value == 'float' && property_exists($settings, $key)){
$settings->{$key} = floatval($settings->{$key});
}
@ -52,12 +52,11 @@ trait SettingsSaver
continue;
}
/*Separate loop if it is a _id field which is an integer cast as a string*/
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && strlen($settings->{$key}) >= 1)) {
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && strlen($settings->{$key}) >= 1)) {
$value = 'integer';
if ($key == 'gmail_sending_user_id') {
if($key == 'gmail_sending_user_id' || $key == 'besr_id')
$value = 'string';
}
if (! property_exists($settings, $key)) {
continue;
@ -99,9 +98,9 @@ trait SettingsSaver
case 'real':
case 'float':
case 'double':
return ! is_string($value) && (is_float($value) || is_numeric(strval($value)));
return !is_string($value) && (is_float($value) || is_numeric(strval($value)));
case 'string':
return ! is_int($value) || (is_string($value) && method_exists($value, '__toString')) || is_null($value) || is_string($value);
return !is_int($value) || ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value);
case 'bool':
case 'boolean':
return is_bool($value) || (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);

View File

@ -60,8 +60,13 @@ class VendorHtmlEngine
$this->company = $invitation->company;
$this->contact = $invitation->contact->load('vendor');
$this->vendor = $this->contact->vendor->load('company', 'country');
$this->vendor = $this->contact->vendor->load('company','country');
if(!$this->vendor->currency_id){
$this->vendor->currency_id = $this->company->settings->currency_id;
$this->vendor->save();
}
$this->entity->load('vendor');
@ -72,35 +77,36 @@ 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;
}
}
public function buildEntityDataArray() :array
{
if (! $this->vendor->currency()) {
if (! $this->vendor->currency()) {
throw new Exception(debug_backtrace()[1]['function'], 1);
exit;
}
@ -125,7 +131,7 @@ class VendorHtmlEngine
$data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.due_date')];
$data['$partial_due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$dueDate'] = &$data['$due_date'];
$data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')];
@ -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) {
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'];
@ -225,13 +233,12 @@ class VendorHtmlEngine
$data['$vat_number'] = ['value' => $this->vendor->vat_number ?: '&nbsp;', 'label' => ctrans('texts.vat_number')];
$data['$website'] = ['value' => $this->vendor->present()->website() ?: '&nbsp;', 'label' => ctrans('texts.website')];
$data['$phone'] = ['value' => $this->vendor->present()->phone() ?: '&nbsp;', 'label' => ctrans('texts.phone')];
$data['$country'] = ['value' => isset($this->vendor->country->name) ? ctrans('texts.country_'.$this->vendor->country->name) : '', 'label' => ctrans('texts.country')];
$data['$country'] = ['value' => isset($this->vendor->country->name) ? ctrans('texts.country_' . $this->vendor->country->name) : '', 'label' => ctrans('texts.country')];
$data['$country_2'] = ['value' => isset($this->vendor->country) ? $this->vendor->country->iso_3166_2 : '', 'label' => ctrans('texts.country')];
$data['$email'] = ['value' => isset($this->contact) ? $this->contact->email : 'no contact email on record', 'label' => ctrans('texts.email')];
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'];
@ -255,7 +262,7 @@ class VendorHtmlEngine
$data['$vendor.postal_city_state'] = &$data['$postal_city_state'];
$data['$vendor.country'] = &$data['$country'];
$data['$vendor.email'] = &$data['$email'];
$data['$vendor.billing_address'] = &$data['$vendor_address'];
$data['$vendor.billing_address1'] = &$data['$vendor.address1'];
$data['$vendor.billing_address2'] = &$data['$vendor.address2'];
@ -361,7 +368,7 @@ class VendorHtmlEngine
$data['_rate2'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['_rate3'] = ['value' => '', 'label' => ctrans('texts.tax')];
$data['$font_size'] = ['value' => $this->settings->font_size.'px', 'label' => ''];
$data['$font_size'] = ['value' => $this->settings->font_size . 'px', 'label' => ''];
$data['$font_name'] = ['value' => Helpers::resolveFont($this->settings->primary_font)['name'], 'label' => ''];
$data['$font_url'] = ['value' => Helpers::resolveFont($this->settings->primary_font)['url'], 'label' => ''];
@ -375,7 +382,7 @@ class VendorHtmlEngine
$data['$entity_footer'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->footer), $this->company), 'label' => ''];
$data['$footer'] = &$data['$entity_footer'];
$data['$page_size'] = ['value' => $this->settings->page_size, 'label' => ''];
$data['$page_layout'] = ['value' => property_exists($this->settings, 'page_layout') ? $this->settings->page_layout : 'Portrait', 'label' => ''];
@ -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')];
@ -477,26 +484,25 @@ class VendorHtmlEngine
$country = Country::find($this->settings->country_id);
if ($country) {
return ctrans('texts.country_'.$country->name);
return ctrans('texts.country_' . $country->name);
}
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
@ -669,8 +675,8 @@ html {
/**
* Generate markup for HTML images on entity.
*
* @return string|void
*
* @return string|void
*/
protected function generateEntityImagesMarkup()
{
@ -680,11 +686,11 @@ html {
$dom = new \DOMDocument('1.0', 'UTF-8');
$container = $dom->createElement('div');
$container = $dom->createElement('div');
$container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr);');
foreach ($this->entity->documents as $document) {
if (! $document->isImage()) {
if (!$document->isImage()) {
continue;
}

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