Merge pull request #8373 from turbo124/v5-develop

v5.5.90
This commit is contained in:
David Bomba 2023-03-15 18:01:11 +11:00 committed by GitHub
commit c36e2b8e26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1761 additions and 1070 deletions

View File

@ -1 +1 @@
5.5.89
5.5.90

View File

@ -12,35 +12,36 @@
namespace App\Console\Commands;
use App;
use Exception;
use App\Models\User;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Account;
use App\Models\Company;
use App\Models\Contact;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\CompanyUser;
use Illuminate\Support\Str;
use App\Models\CompanyToken;
use App\Models\ClientContact;
use App\Models\CompanyLedger;
use App\Models\PurchaseOrder;
use App\Models\VendorContact;
use App\Models\QuoteInvitation;
use Illuminate\Console\Command;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\DataMapper\ClientSettings;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use App\Factory\ClientContactFactory;
use App\Factory\VendorContactFactory;
use App\Jobs\Company\CreateCompanyToken;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyLedger;
use App\Models\CompanyUser;
use App\Models\Contact;
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Utils\Ninja;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption;
/*
@ -160,16 +161,33 @@ class CheckData extends Command
private function checkCompanyTokens()
{
CompanyUser::doesnthave('token')->cursor()->each(function ($cu) {
if ($cu->user) {
// CompanyUser::whereDoesntHave('token', function ($query){
// return $query->where('is_system', 1);
// })->cursor()->each(function ($cu){
// if ($cu->user) {
// $this->logMessage("Creating missing company token for user # {$cu->user->id} for company id # {$cu->company->id}");
// (new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle();
// } else {
// $this->logMessage("Dangling User ID # {$cu->id}");
// }
// });
CompanyUser::query()->cursor()->each(function ($cu) {
if (CompanyToken::where('user_id', $cu->user_id)->where('company_id', $cu->company_id)->where('is_system', 1)->doesntExist()) {
$this->logMessage("Creating missing company token for user # {$cu->user->id} for company id # {$cu->company->id}");
(new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle();
} else {
$this->logMessage("Dangling User ID # {$cu->id}");
(new CreateCompanyToken($cu->company, $cu->user, 'System'))->handle();
}
});
}
}
/**
* checkOauthSanity
*
* @return void
*/
private function checkOauthSanity()
{
User::where('oauth_provider_id', '1')->cursor()->each(function ($user) {

View File

@ -467,7 +467,16 @@ class CompanySettings extends BaseSettings
public $show_task_item_description = false;
public $client_initiated_payments = false;
public $client_initiated_payments_minimum = 0;
public $sync_invoice_quote_columns = true;
public static $casts = [
'client_initiated_payments' => 'bool',
'client_initiated_payments_minimum' => 'float',
'sync_invoice_quote_columns' => 'bool',
'show_task_item_description' => 'bool',
'allow_billable_task_items' => 'bool',
'accept_client_input_quote_approval' => 'bool',
@ -907,6 +916,15 @@ class CompanySettings extends BaseSettings
'$product.tax',
'$product.line_total',
],
'product_quote_columns' => [
'$product.item',
'$product.description',
'$product.unit_cost',
'$product.quantity',
'$product.discount',
'$product.tax',
'$product.line_total',
],
'task_columns' =>[
'$task.service',
'$task.description',

View File

@ -85,7 +85,10 @@ class CreditFilters extends QueryFilters
->orWhere('credits.custom_value1', 'like', '%'.$filter.'%')
->orWhere('credits.custom_value2', 'like', '%'.$filter.'%')
->orWhere('credits.custom_value3', 'like', '%'.$filter.'%')
->orWhere('credits.custom_value4', 'like', '%'.$filter.'%');
->orWhere('credits.custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter){
$q->where('name', 'like', '%'.$filter.'%');
});
});
}

View File

@ -69,6 +69,7 @@ class InvoiceFilters extends QueryFilters
if (in_array('overdue', $status_parameters)) {
$query->orWhereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('due_date', '<', Carbon::now())
->orWhere('due_date', null)
->orWhere('partial_due_date', '<', Carbon::now());
}
});
@ -107,7 +108,10 @@ class InvoiceFilters extends QueryFilters
->orWhere('custom_value1', 'like', '%'.$filter.'%')
->orWhere('custom_value2', 'like', '%'.$filter.'%')
->orWhere('custom_value3', 'like', '%'.$filter.'%')
->orWhere('custom_value4', 'like', '%'.$filter.'%');
->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter){
$q->where('name', 'like', '%'.$filter.'%');
});
});
}

View File

@ -93,7 +93,10 @@ class PurchaseOrderFilters extends QueryFilters
->orWhere('custom_value1', 'like', '%'.$filter.'%')
->orWhere('custom_value2', 'like', '%'.$filter.'%')
->orWhere('custom_value3', 'like', '%'.$filter.'%')
->orWhere('custom_value4', 'like', '%'.$filter.'%');
->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('vendor', function ($q) use ($filter){
$q->where('name', 'like', '%'.$filter.'%');
});
});
}

View File

@ -37,7 +37,10 @@ class QuoteFilters extends QueryFilters
->orwhere('custom_value1', 'like', '%'.$filter.'%')
->orWhere('custom_value2', 'like', '%'.$filter.'%')
->orWhere('custom_value3', 'like', '%'.$filter.'%')
->orWhere('custom_value4', 'like', '%'.$filter.'%');
->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('client', function ($q) use ($filter){
$q->where('name', 'like', '%'.$filter.'%');
});
});
}

View File

@ -154,7 +154,7 @@ class AccountController extends BaseController
$truth->setUser(auth()->user());
$truth->setCompany($ct->first()->company);
return $this->listResponse($ct);
return $this->listResponse($ct->fresh());
}
public function update(UpdateAccountRequest $request, Account $account)

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -15,7 +16,6 @@ use App\DataMapper\Analytics\LoginFailure;
use App\DataMapper\Analytics\LoginSuccess;
use App\Events\User\UserLoggedIn;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\Controller;
use App\Http\Requests\Login\LoginRequest;
use App\Jobs\Account\CreateAccount;
use App\Jobs\Company\CreateCompanyToken;
@ -23,8 +23,6 @@ use App\Libraries\MultiDB;
use App\Libraries\OAuth\OAuth;
use App\Libraries\OAuth\Providers\Google;
use App\Models\Account;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\CompanyUser;
use App\Models\User;
@ -38,7 +36,6 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Laravel\Socialite\Facades\Socialite;
use Microsoft\Graph\Model;
use PragmaRX\Google2FA\Google2FA;
@ -46,18 +43,7 @@ use Turbo124\Beacon\Facades\LightLogs;
class LoginController extends BaseController
{
/**
* @OA\Tag(
* name="login",
* description="Authentication",
* @OA\ExternalDocumentation(
* description="Find out more",
* url="https://invoiceninja.github.io"
* )
* )
*/
use AuthenticatesUsers;
use UserSessionAttributes;
use LoginCache;
@ -89,7 +75,7 @@ class LoginController extends BaseController
* @param Request $request
* @param User $user
* @return void
* deprecated .1 API ONLY we don't need to set any session variables
* @deprecated .1 API ONLY we don't need to set any session variables
*/
public function authenticated(Request $request, User $user): void
{
@ -99,63 +85,8 @@ class LoginController extends BaseController
/**
* Login via API.
*
* @param Request $request The request
*
* @return Response|User Process user login.
*
* @param LoginRequest $request The request
* @throws \Illuminate\Validation\ValidationException
* @OA\Post(
* path="/api/v1/login",
* operationId="postLogin",
* tags={"login"},
* summary="Attempts authentication",
* description="Returns a CompanyUser object on success",
* @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(ref="#/components/parameters/include_static"),
* @OA\Parameter(ref="#/components/parameters/clear_cache"),
* @OA\RequestBody(
* description="User credentials",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="object",
* @OA\Property(
* property="email",
* description="The user email address",
* type="string",
* ),
* @OA\Property(
* property="password",
* example="1234567",
* description="The user password must meet minimum criteria ~ >6 characters",
* type="string"
* )
* )
* )
* ),
* @OA\Response(
* response=200,
* description="The Company User 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\JsonContent(ref="#/components/schemas/CompanyUser"),
* ),
* @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 apiLogin(LoginRequest $request)
{
@ -175,7 +106,7 @@ class LoginController extends BaseController
if ($this->attemptLogin($request)) {
LightLogs::create(new LoginSuccess())
->increment()
->queue();
->batch();
$user = $this->guard()->user();
@ -221,7 +152,7 @@ class LoginController extends BaseController
} else {
LightLogs::create(new LoginFailure())
->increment()
->queue();
->batch();
$this->incrementLoginAttempts($request);
@ -236,39 +167,7 @@ class LoginController extends BaseController
* Refreshes the data feed with the current Company User.
*
* @param Request $request
* @return CompanyUser Refresh Feed.
*
*
* @OA\Post(
* path="/api/v1/refresh",
* operationId="refresh",
* tags={"refresh"},
* summary="Refreshes the dataset",
* description="Refreshes the dataset",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(ref="#/components/parameters/include_static"),
* @OA\Parameter(ref="#/components/parameters/clear_cache"),
* @OA\Response(
* response=200,
* description="The Company User 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\JsonContent(ref="#/components/schemas/CompanyUser"),
* ),
* @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"),
* ),
* )
* @return CompanyUser Refresh Feed.
*/
public function refresh(Request $request)
{
@ -346,7 +245,7 @@ class LoginController extends BaseController
private function handleSocialiteLogin($provider, $token)
{
$user = $this->getSocialiteUser($provider, $token);
if ($user) {
return $this->loginOrCreateFromSocialite($user, $provider);
}
@ -363,7 +262,7 @@ class LoginController extends BaseController
'oauth_user_id' => $user->id,
'oauth_provider_id' => $provider,
];
if ($existing_user = MultiDB::hasUser($query)) {
if (!$existing_user->account) {
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
@ -408,7 +307,7 @@ class LoginController extends BaseController
return $this->timeConstrainedResponse($cu);
}
nlog("socialite");
nlog($user);
@ -478,7 +377,7 @@ class LoginController extends BaseController
if (auth()->user()->company_users()->count() != auth()->user()->tokens()->distinct('company_id')->count()) {
auth()->user()->companies->each(function ($company) {
if (!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->exists()) {
if (!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->where('is_system',true)->exists()) {
(new CreateCompanyToken($company, auth()->user(), 'Google_O_Auth'))->handle();
}
});
@ -499,7 +398,6 @@ class LoginController extends BaseController
return response()->json(['message' => 'Invalid response from oauth server, no access token in response.'], 400);
}
$graph = new \Microsoft\Graph\Graph();
$graph->setAccessToken($accessToken);
@ -536,17 +434,22 @@ class LoginController extends BaseController
return $this->existingLoginUser($user->getId(), 'microsoft');
}
// Signup!
$new_account = [
'first_name' => $user->getGivenName() ?: '',
'last_name' => $user->getSurname() ?: '',
'password' => '',
'email' => $email,
'oauth_user_id' => $user->getId(),
'oauth_provider_id' => 'microsoft',
];
return $this->createNewAccount($new_account);
// Signup!
if (request()->has('create') && request()->input('create') == 'true') {
$new_account = [
'first_name' => $user->getGivenName() ?: '',
'last_name' => $user->getSurname() ?: '',
'password' => '',
'email' => $email,
'oauth_user_id' => $user->getId(),
'oauth_provider_id' => 'microsoft',
];
return $this->createNewAccount($new_account);
}
return response()->json(['message' => 'User not found. If you believe this is an error, please send an email to contact@invoiceninja.com'], 400);
}
@ -640,19 +543,23 @@ class LoginController extends BaseController
return $this->existingLoginUser($google->harvestSubField($user), 'google');
}
//user not found anywhere - lets sign them up.
$name = OAuth::splitName($google->harvestName($user));
if (request()->has('create') && request()->input('create') == 'true') {
//user not found anywhere - lets sign them up.
$name = OAuth::splitName($google->harvestName($user));
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id' => 'google',
];
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id' => 'google',
];
return $this->createNewAccount($new_account);
return $this->createNewAccount($new_account);
}
return response()->json(['message' => 'User not found. If you believe this is an error, please send an email to contact@invoiceninja.com'], 400);
}
return response()
@ -700,7 +607,7 @@ class LoginController extends BaseController
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"];
$parameters = ['response_type' => 'code', 'redirect_uri' => config('ninja.app_url') . "/auth/microsoft"];
}
if (request()->has('code')) {

View File

@ -1,81 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'first_name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\User
*/
protected function create(array $data)
{
return User::create([
'first_name' => $data['first_name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
}

View File

@ -28,16 +28,13 @@ class VendorContactLoginController extends Controller
public function catch()
{
$data = [
];
return $this->render('purchase_orders.catch');
}
public function logout()
{
Auth::guard('vendor')->logout();
request()->session()->invalidate();
return redirect('/vendors');

View File

@ -1,50 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Auth;
use Illuminate\Foundation\Auth\VerifiesEmails;
use Illuminate\Routing\Controller;
class VerificationController extends Controller
{
/*
|--------------------------------------------------------------------------
| Email Verification Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling email verification for any
| user that recently registered with the application. Emails may also
| be resent if the user did not receive the original email message.
|
*/
use VerifiesEmails;
/**
* Where to redirect users after verification.
*
* @var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
}

View File

@ -31,6 +31,7 @@ use App\Http\Requests\Email\SendEmailRequest;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\RecurringInvoiceTransformer;
use Illuminate\Mail\Mailables\Address;
class EmailController extends BaseController
{
@ -135,6 +136,8 @@ class EmailController extends BaseController
$mo->email_template_body = $request->input('template');
$mo->email_template_subject = str_replace("template", "subject", $request->input('template'));
if($request->has('cc_email'))
$mo->cc[] = new Address($request->cc_email);
// if ($entity == 'purchaseOrder' || $entity == 'purchase_order' || $template == 'purchase_order' || $entity == 'App\Models\PurchaseOrder') {
// return $this->sendPurchaseOrder($entity_obj, $data, $template);

View File

@ -11,27 +11,29 @@
namespace App\Http\Controllers;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
use App\Events\RecurringInvoice\RecurringInvoiceWasUpdated;
use App\Utils\Ninja;
use App\Models\Account;
use Illuminate\Http\Response;
use App\Utils\Traits\MakesHash;
use App\Models\RecurringInvoice;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Support\Facades\Storage;
use App\Factory\RecurringInvoiceFactory;
use App\Filters\RecurringInvoiceFilters;
use App\Http\Requests\RecurringInvoice\ActionRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\CreateRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\DestroyRecurringInvoiceRequest;
use App\Jobs\RecurringInvoice\UpdateRecurring;
use App\Repositories\RecurringInvoiceRepository;
use App\Transformers\RecurringInvoiceTransformer;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
use App\Events\RecurringInvoice\RecurringInvoiceWasUpdated;
use App\Http\Requests\RecurringInvoice\BulkRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\EditRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\ShowRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\StoreRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\ActionRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\CreateRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\UpdateRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\UploadRecurringInvoiceRequest;
use App\Models\Account;
use App\Models\RecurringInvoice;
use App\Repositories\RecurringInvoiceRepository;
use App\Transformers\RecurringInvoiceTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use App\Http\Requests\RecurringInvoice\DestroyRecurringInvoiceRequest;
/**
* Class RecurringInvoiceController.
@ -392,50 +394,6 @@ class RecurringInvoiceController extends BaseController
*
* @param DestroyRecurringInvoiceRequest $request
* @param RecurringInvoice $recurring_invoice
*
* @return Response
*
*
* @throws \Exception
* @OA\Delete(
* path="/api/v1/recurring_invoices/{id}",
* operationId="deleteRecurringInvoice",
* tags={"recurring_invoices"},
* summary="Deletes a RecurringInvoice",
* description="Handles the deletion of an RecurringInvoice by id",
* @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 RecurringInvoice Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns a HTTP status",
* @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 destroy(DestroyRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice)
{
@ -445,195 +403,31 @@ class RecurringInvoiceController extends BaseController
}
/**
* @OA\Get(
* path="/api/v1/recurring_invoice/{invitation_key}/download",
* operationId="downloadRecurringInvoice",
* tags={"invoices"},
* summary="Download a specific invoice by invitation key",
* description="Downloads a specific invoice",
* @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="invitation_key",
* in="path",
* description="The Recurring Invoice Invitation Key",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the recurring invoice pdf",
* @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"),
* ),
* )
* @param $invitation_key
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadPdf($invitation_key)
public function bulk(BulkRecurringInvoiceRequest $request)
{
$invitation = $this->recurring_invoice_repo->getInvitationByKey($invitation_key);
$contact = $invitation->contact;
$recurring_invoice = $invitation->recurring_invoice;
$file = $recurring_invoice->service()->getInvoicePdf($contact);
$percentage_increase = request()->has('percentage_increase') ? request()->input('percentage_increase') : 0;
return response()->streamDownload(function () use ($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
}
if(in_array($request->action, ['increase_prices', 'update_prices'])) {
UpdateRecurring::dispatch($request->ids, auth()->user()->company(), auth()->user(), $request->action, $percentage_increase);
/**
* Perform bulk actions on the list view.
*
* @return Collection
*
*
* @OA\Post(
* path="/api/v1/recurring_invoices/bulk",
* operationId="bulkRecurringInvoices",
* tags={"recurring_invoices"},
* summary="Performs bulk actions on an array of recurring_invoices",
* description="",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/index"),
* @OA\RequestBody(
* description="Hashed IDs",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="array",
* @OA\Items(
* type="integer",
* description="Array of hashed IDs to be bulk 'actioned",
* example="[0,1,2,3]",
* ),
* )
* )
* ),
* @OA\Response(
* response=200,
* description="The RecurringInvoice 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\JsonContent(ref="#/components/schemas/RecurringInvoice"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
return response()->json(['message' => 'Update in progress.'], 200);
}
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function bulk()
{
$action = request()->input('action');
$recurring_invoices = RecurringInvoice::withTrashed()->find($request->ids);
$ids = request()->input('ids');
$recurring_invoices = RecurringInvoice::withTrashed()->find($this->transformKeys($ids));
$recurring_invoices->each(function ($recurring_invoice, $key) use ($action) {
$recurring_invoices->each(function ($recurring_invoice, $key) use($request){
if (auth()->user()->can('edit', $recurring_invoice)) {
$this->performAction($recurring_invoice, $action, true);
$this->performAction($recurring_invoice, $request->action, true);
}
});
return $this->listResponse(RecurringInvoice::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return $this->listResponse(RecurringInvoice::withTrashed()->whereIn('id', $request->ids));
}
/**
* Recurring Invoice Actions.
*
*
* @OA\Get(
* path="/api/v1/recurring_invoices/{id}/{action}",
* operationId="actionRecurringInvoice",
* tags={"recurring_invoices"},
* summary="Performs a custom action on an RecurringInvoice",
* description="Performs a custom action on an RecurringInvoice.
The current range of actions are as follows
- clone_to_RecurringInvoice
- clone_to_quote
- history
- delivery_note
- mark_paid
- download
- archive
- delete
- email",
* @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 RecurringInvoice Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Parameter(
* name="action",
* in="path",
* description="The action string to be performed",
* example="clone_to_quote",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the RecurringInvoice 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/RecurringInvoice"),
* ),
* @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"),
* ),
* )
* @param ActionRecurringInvoiceRequest $request
* @param RecurringInvoice $recurring_invoice
* @param $action

View File

@ -592,9 +592,9 @@ class UserController extends BaseController
*/
public function detach(DetachCompanyUserRequest $request, User $user)
{
if ($request->entityIsDeleted($user)) {
return $request->disallowUpdate();
}
// if ($request->entityIsDeleted($user)) {
// return $request->disallowUpdate();
// }
$company_user = CompanyUser::whereUserId($user->id)
->whereCompanyId(auth()->user()->companyId())

View File

@ -232,8 +232,8 @@ class RequiredClientInfo extends Component
if ($cg && $cg->update_details) {
$payment_gateway = $cg->driver($this->client)->init();
// if(method_exists($payment_gateway, "updateCustomer"))
// $payment_gateway->updateCustomer();
if(method_exists($payment_gateway, "updateCustomer"))
$payment_gateway->updateCustomer();
}
return true;

View File

@ -43,6 +43,7 @@ class SendEmailRequest extends Request
'template' => 'bail|required',
'entity' => 'bail|required',
'entity_id' => 'bail|required',
'cc_email' => 'bail|sometimes|email',
];
}

View File

@ -0,0 +1,51 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\RecurringInvoice;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class BulkRecurringInvoiceRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return true;
}
public function rules()
{
return [
'ids' => ['required','bail','array',Rule::exists('recurring_invoices', 'id')->where('company_id', auth()->user()->company()->id)],
'action' => 'in:archive,restore,delete,increase_prices,update_prices,start,stop,send_now',
'percentage_increase' => 'required_if:action,increase_prices|numeric|min:0|max:100',
];
}
public function prepareForValidation()
{
$input = $this->all();
if (isset($input['ids'])) {
$input['ids'] = $this->transformKeys($input['ids']);
}
$this->replace($input);
}
}

View File

@ -12,14 +12,10 @@
namespace App\Http\Requests\User;
use App\Http\Requests\Request;
use App\Models\User;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
class DetachCompanyUserRequest extends Request
{
use MakesHash;
use ChecksEntityStatus;
/**
* Determine if the user is authorized to make this request.

View File

@ -126,7 +126,7 @@ class CreateAccount
NinjaMailerJob::dispatch($nmo, true);
\Modules\Admin\Jobs\Account\NinjaUser::dispatch([], $sp035a66);
(new \Modules\Admin\Jobs\Account\NinjaUser([], $sp035a66))->handle();
}
VersionCheck::dispatch();

View File

@ -31,17 +31,8 @@ class AdjustProductInventory implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, UserNotifies;
public Company $company;
public Invoice $invoice;
public array $old_invoice;
public function __construct(Company $company, Invoice $invoice, $old_invoice = [])
public function __construct(public Company $company, public Invoice $invoice, public $old_invoice = [])
{
$this->company = $company;
$this->invoice = $invoice;
$this->old_invoice = $old_invoice;
}
/**
@ -65,33 +56,64 @@ class AdjustProductInventory implements ShouldQueue
{
MultiDB::setDb($this->company->db);
foreach ($this->invoice->line_items as $item) {
$p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->first();
// foreach ($this->invoice->line_items as $item) {
// $p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->first();
// if (! $p) {
// continue;
// }
// $p->in_stock_quantity += $item->quantity;
// $p->saveQuietly();
// }
collect($this->invoice->line_items)->filter(function ($item){
return $item->type_id == '1';
})->each(function ($i){
$p = Product::where('product_key', $i->product_key)->where('company_id', $this->company->id)->first();
if ($p) {
$p->in_stock_quantity += $i->quantity;
$p->saveQuietly();
if (! $p) {
continue;
}
$p->in_stock_quantity += $item->quantity;
});
$p->saveQuietly();
}
}
public function handleRestoredInvoice()
{
MultiDB::setDb($this->company->db);
foreach ($this->invoice->line_items as $item) {
$p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->first();
// foreach ($this->invoice->line_items as $item) {
// $p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->first();
if (! $p) {
continue;
// if (! $p) {
// continue;
// }
// $p->in_stock_quantity -= $item->quantity;
// $p->saveQuietly();
// }
collect($this->invoice->line_items)->filter(function ($item) {
return $item->type_id == '1';
})->each(function ($i) {
$p = Product::where('product_key', $i->product_key)->where('company_id', $this->company->id)->first();
if ($p) {
$p->in_stock_quantity -= $i->quantity;
$p->saveQuietly();
}
});
$p->in_stock_quantity -= $item->quantity;
$p->saveQuietly();
}
}
public function middleware()
@ -101,38 +123,74 @@ class AdjustProductInventory implements ShouldQueue
private function newInventoryAdjustment()
{
$line_items = $this->invoice->line_items;
// $line_items = $this->invoice->line_items;
foreach ($line_items as $item) {
$p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->where('in_stock_quantity', '>', 0)->first();
// foreach ($line_items as $item) {
// $p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->where('in_stock_quantity', '>', 0)->first();
// if (! $p) {
// continue;
// }
// $p->in_stock_quantity -= $item->quantity;
// $p->saveQuietly();
// if ($this->company->stock_notification && $p->stock_notification && $p->stock_notification_threshold && $p->in_stock_quantity <= $p->stock_notification_threshold) {
// $this->notifyStockLevels($p, 'product');
// } elseif ($this->company->stock_notification && $p->stock_notification && $this->company->inventory_notification_threshold && $p->in_stock_quantity <= $this->company->inventory_notification_threshold) {
// $this->notifyStocklevels($p, 'company');
// }
// }
collect($this->invoice->line_items)->filter(function ($item) {
return $item->type_id == '1';
})->each(function ($i) {
$p = Product::where('product_key', $i->product_key)->where('company_id', $this->company->id)->first();
if ($p) {
$p->in_stock_quantity -= $i->quantity;
$p->saveQuietly();
if ($this->company->stock_notification && $p->stock_notification && $p->stock_notification_threshold && $p->in_stock_quantity <= $p->stock_notification_threshold) {
$this->notifyStockLevels($p, 'product');
} elseif ($this->company->stock_notification && $p->stock_notification && $this->company->inventory_notification_threshold && $p->in_stock_quantity <= $this->company->inventory_notification_threshold) {
$this->notifyStocklevels($p, 'company');
}
if (! $p) {
continue;
}
});
$p->in_stock_quantity -= $item->quantity;
$p->saveQuietly();
if ($this->company->stock_notification && $p->stock_notification && $p->stock_notification_threshold && $p->in_stock_quantity <= $p->stock_notification_threshold) {
$this->notifyStockLevels($p, 'product');
} elseif ($this->company->stock_notification && $p->stock_notification && $this->company->inventory_notification_threshold && $p->in_stock_quantity <= $this->company->inventory_notification_threshold) {
$this->notifyStocklevels($p, 'company');
}
}
}
private function existingInventoryAdjustment()
{
foreach ($this->old_invoice as $item) {
$p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->first();
// foreach ($this->old_invoice as $item) {
// $p = Product::where('product_key', $item->product_key)->where('company_id', $this->company->id)->first();
if (! $p) {
continue;
// if (! $p) {
// continue;
// }
// $p->in_stock_quantity += $item->quantity;
// $p->saveQuietly();
// }
collect($this->invoice->line_items)->filter(function ($item) {
return $item->type_id == '1';
})->each(function ($i) {
$p = Product::where('product_key', $i->product_key)->where('company_id', $this->company->id)->first();
if ($p) {
$p->in_stock_quantity += $i->quantity;
$p->saveQuietly();
}
});
$p->in_stock_quantity += $item->quantity;
$p->saveQuietly();
}
}
private function notifyStocklevels(Product $product, string $notification_level)

View File

@ -214,13 +214,11 @@ class SendReminders implements ShouldQueue
nlog('firing email');
EmailEntity::dispatch($invitation, $invitation->company, $template)->delay(10);
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $template));
}
});
if ($this->checkSendSetting($invoice, $template)) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $template));
}
$invoice->last_sent_date = now();
$invoice->next_send_date = $this->calculateNextSendDate($invoice);

View File

@ -0,0 +1,61 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\RecurringInvoice;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\RecurringInvoice;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class UpdateRecurring implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public function __construct(public array $ids, public Company $company, public User $user, protected string $action, protected float $percentage = 0)
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle() : void
{
MultiDB::setDb($this->company->db);
RecurringInvoice::where('company_id', $this->company->id)
->whereIn('id', $this->ids)
->chunk(100, function ($recurring_invoices) {
foreach ($recurring_invoices as $recurring_invoice) {
if ($this->user->can('edit', $recurring_invoice)) {
if ($this->action == 'update_prices') {
$recurring_invoice->service()->updatePrice();
} elseif ($this->action == 'increase_prices') {
$recurring_invoice->service()->increasePrice($this->percentage);
}
}
}
});
}
public function failed($exception = null)
{
}
}

View File

@ -32,7 +32,7 @@ class PlayStoreRenewSubscription implements ShouldQueue
$expirationTime = $event->getSubscription()->getExpiryTime();
$account = Account::where('inapp_transaction_id', $in_app_identifier)->first();
$account = Account::where('inapp_transaction_id', 'like', $in_app_identifier."%")->first();
if ($account) {
$account->update(['plan_expires' => Carbon::parse($expirationTime)]);

View File

@ -474,7 +474,7 @@ class Client extends BaseModel implements HasLocalePreference
* of settings which have been merged from
* Client > Group > Company levels.
*
* @return stdClass stdClass object of settings
* @return \stdClass stdClass object of settings
*/
public function getMergedSettings() :object
{

View File

@ -735,7 +735,9 @@ class RecurringInvoice extends BaseModel
}
/**
* Service entry points.
* service
*
* @return RecurringService
*/
public function service() :RecurringService
{

View File

@ -100,8 +100,8 @@ class CreditCard
*/
public function paymentResponse(PaymentResponseRequest $request)
{
// nlog($request->all());
$this->braintree->client->fresh();
$state = [
'server_response' => json_decode($request->gateway_response),
'payment_hash' => $request->payment_hash,
@ -124,14 +124,15 @@ class CreditCard
'options' => [
'submitForSettlement' => true,
],
'billing' => [
'streetAddress' => $this->braintree->client->address1 ?: '',
'extendedAddress' =>$this->braintree->client->address2 ?: '',
'locality' => $this->braintree->client->city ?: '',
'postalCode' => $this->braintree->client->postal_code ?: '',
'countryCodeAlpha2' => $this->braintree->client->country ? $this->braintree->client->country->iso_3166_2 : 'US',
]
];
// uses the same auth id twice when this is enabled.
// if($state['server_response']?->threeDSecureInfo){
// $data['threeDSecureAuthenticationId'] = $state['server_response']?->threeDSecureInfo?->threeDSecureAuthenticationId;
// }
if ($this->braintree->company_gateway->getConfigField('merchantAccountId')) {
/** https://developer.paypal.com/braintree/docs/reference/request/transaction/sale/php#full-example */
$data['merchantAccountId'] = $this->braintree->company_gateway->getConfigField('merchantAccountId');
@ -139,6 +140,7 @@ class CreditCard
try {
$result = $this->braintree->gateway->transaction()->sale($data);
} catch (\Exception $e) {
if ($e instanceof \Braintree\Exception\Authorization) {
$this->braintree->sendFailureMail(ctrans('texts.generic_gateway_error'));
@ -182,6 +184,13 @@ class CreditCard
'options' => [
'verifyCard' => true,
],
'billingAddress' => [
'streetAddress' => $this->braintree->client->address1 ?: '',
'extendedAddress' =>$this->braintree->client->address2 ?: '',
'locality' => $this->braintree->client->city ?: '',
'postalCode' => $this->braintree->client->postal_code ?: '',
'countryCodeAlpha2' => $this->braintree->client->country ? $this->braintree->client->country->iso_3166_2 : 'US',
]
];
if ($this->braintree->company_gateway->getConfigField('merchantAccountId')) {

View File

@ -48,7 +48,7 @@ class BraintreePaymentDriver extends BaseDriver
const SYSTEM_LOG_TYPE = SystemLog::TYPE_BRAINTREE;
public function init(): void
public function init(): self
{
$this->gateway = new Gateway([
'environment' => $this->company_gateway->getConfigField('testMode') ? 'sandbox' : 'production',
@ -56,6 +56,8 @@ class BraintreePaymentDriver extends BaseDriver
'publicKey' => $this->company_gateway->getConfigField('publicKey'),
'privateKey' => $this->company_gateway->getConfigField('privateKey'),
]);
return $this;
}
public function setPaymentMethod($payment_method_id)
@ -109,6 +111,12 @@ class BraintreePaymentDriver extends BaseDriver
return $this->gateway->customer()->find($existing->gateway_customer_reference);
}
$customer = $this->searchByEmail();
if ($customer) {
return $customer;
}
$result = $this->gateway->customer()->create([
'firstName' => $this->client->present()->name(),
'email' => $this->client->present()->email(),
@ -138,6 +146,45 @@ class BraintreePaymentDriver extends BaseDriver
SystemLogger::dispatch(['server_response' => $result, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_BRAINTREE, $this->client, $this->client->company);
}
private function searchByEmail()
{
$result = $this->gateway->customer()->search([
\Braintree\CustomerSearch::email()->is($this->client->present()->email()),
]);
if ($result->maximumCount() > 0) {
return $result->firstItem();
}
}
// public function updateCustomer()
// {
// $customer = $this->findOrCreateCustomer();
// $result = $this->gateway->customer()->update(
// $customer->id,
// [
// 'firstName' => $this->client->present()->name(),
// 'email' => $this->client->present()->email(),
// 'phone' => $this->client->present()->phone(),
// 'creditCard' => [
// 'billingAddress' => [
// 'options' => [
// 'updateExisting' => true
// ],
// 'firstName' => $this->client->present()->first_name() ?: $this->client->present()->name(),
// 'lastName' => $this->client->present()->last_name() ?: '',
// 'streetAddress' => $this->client->address1 ?: '',
// 'extendedAddress' =>$this->client->address2 ?: '',
// 'locality' => $this->client->city ?: '',
// 'postalCode' => $this->client->postal_code ?: '',
// 'countryCodeAlpha2' => $this->client->country ? $this->client->country->iso_3166_2 : 'US',
// ],
// ],
// ]
// );
// }
public function refund(Payment $payment, $amount, $return_client_response = false)
{
$this->init();

View File

@ -100,7 +100,7 @@ class PaymentMigrationRepository extends BaseRepository
$payment->deleted_at = $data['deleted_at'] ?: null;
$payment->save();
if (array_key_exists('currency_id', $data) && $data['currency_id'] == 0) {
if ($payment->currency_id == 0) {
$payment->currency_id = $payment->company->settings->currency_id;
$payment->save();
}

View File

@ -261,11 +261,33 @@ class Email implements ShouldQueue
LightLogs::create(new EmailSuccess($this->company->company_key))
->send();
} catch(\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch(\Symfony\Component\Mime\Exception\LogicException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch (\Exception | \RuntimeException | \Google\Service\Exception $e) {
nlog("Mailer failed with {$e->getMessage()}");
nlog("Mailer failed with {$e->getMessage()}");
$message = $e->getMessage();
if (stripos($e->getMessage(), 'code 406') || stripos($e->getMessage(), 'code 300') || stripos($e->getMessage(), 'code 413')) {
$message = "Either Attachment too large, or recipient has been suppressed.";
$this->fail();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
$this->cleanUpMailers();
return;
}
/**
* Post mark buries the proper message in a a guzzle response
* this merges a text string with a json object

View File

@ -69,6 +69,7 @@ class EmailDefaults
$this->setLocale()
->setFrom()
->setTo()
->setCc()
->setTemplate()
->setBody()
->setSubject()
@ -127,7 +128,15 @@ class EmailDefaults
private function setFrom(): self
{
if (Ninja::isHosted() && $this->email->email_object->settings->email_sending_method == 'default') {
$this->email->email_object->from = new Address(config('mail.from.address'), $this->email->company->owner()->name());
if ($this->email->company->account->isPaid() && property_exists($this->email->email_object->settings, 'email_from_name') && strlen($this->email->email_object->settings->email_from_name) > 1) {
$email_from_name = $this->email->email_object->settings->email_from_name;
} else {
$email_from_name = $this->email->company->present()->name();
}
$this->email->email_object->from = new Address(config('mail.from.address'), $email_from_name);
return $this;
}
@ -251,13 +260,14 @@ class EmailDefaults
/**
* Sets the CC of the email
* @todo at some point....
*/
private function buildCc()
private function setCc(): self
{
return [
return $this;
// return $this->email->email_object->cc;
// return [
];
// ];
}
/**
@ -273,7 +283,7 @@ class EmailDefaults
$documents = [];
/* Return early if the user cannot attach documents */
if (!$this->email->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) {
if (!$this->email->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
return $this;
}
@ -304,7 +314,7 @@ class EmailDefaults
}
}
if(!$this->email->email_object->settings->document_email_attachment)
if(!$this->email->email_object->settings->document_email_attachment || !$this->email->company->account->hasFeature(Account::FEATURE_DOCUMENTS))
return $this;
/* Company Documents */

View File

@ -39,14 +39,15 @@ class EmailMailable extends Mailable
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
{nlog($this->email_object->cc);
return new Envelope(
subject: $this->email_object->subject,
tags: [$this->email_object->company_key],
replyTo: $this->email_object->reply_to,
from: $this->email_object->from,
to: $this->email_object->to,
bcc: $this->email_object->bcc
bcc: $this->email_object->bcc,
cc: $this->email_object->cc,
);
}

View File

@ -445,6 +445,14 @@ class PdfBuilder
return $elements;
}
$_type = Str::startsWith($type, '$') ? ltrim($type, '$') : $type;
$table_type = "{$_type}_columns";
if ($_type == 'product' && $this->service->config->entity instanceof Quote && !$this->service->config->settings?->sync_invoice_quote_columns) {
$table_type = "product_quote_columns";
}
foreach ($items as $row) {
$element = ['element' => 'tr', 'elements' => []];
@ -645,7 +653,13 @@ class PdfBuilder
'$task.rate' => '$task.cost',
];
foreach ($this->service->config->pdf_variables["{$type}_columns"] as $column) {
$table_type = "{$type}_columns";
if ($type == 'product' && $this->service->config->entity instanceof Quote && !$this->service->config->settings_object?->sync_invoice_quote_columns) {
$table_type = "product_quote_columns";
}
foreach ($this->service->config->pdf_variables[$table_type] 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->service->config->settings->hide_empty_columns_on_pdf]];
} elseif ($column == '$product.discount' && !$this->service->company->enable_product_discount) {

File diff suppressed because it is too large Load Diff

View File

@ -679,7 +679,12 @@ class Design extends BaseDesign
'$task.rate' => '$task.cost',
];
foreach ($this->context['pdf_variables']["{$type}_columns"] as $column) {
$table_type = "{$type}_columns";
if($type == 'product' && $this->entity instanceof Quote && !$this->settings_object->getSetting('sync_invoice_quote_columns'))
$table_type = "product_quote_columns";
foreach ($this->context['pdf_variables'][$table_type] 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) {
@ -748,6 +753,14 @@ class Design extends BaseDesign
return $elements;
}
$_type = Str::startsWith($type, '$') ? ltrim($type, '$') : $type;
$table_type = "{$_type}_columns";
if ($_type == 'product' && $this->entity instanceof Quote && !$this->settings_object->getSetting('sync_invoice_quote_columns')) {
$table_type = "product_quote_columns";
}
foreach ($items as $row) {
$element = ['element' => 'tr', 'elements' => []];
@ -775,9 +788,8 @@ class Design extends BaseDesign
}
}
} else {
$_type = Str::startsWith($type, '$') ? ltrim($type, '$') : $type;
foreach ($this->context['pdf_variables']["{$_type}_columns"] as $key => $cell) {
foreach ($this->context['pdf_variables'][$table_type] as $key => $cell) {
// We want to keep aliases like these:
// $task.cost => $task.rate
// $task.quantity => $task.hours

View File

@ -13,8 +13,7 @@ namespace App\Services\Quote;
use App\Jobs\Entity\EmailEntity;
use App\Models\ClientContact;
use App\Services\Email\MailEntity;
use App\Services\Email\MailObject;
class SendEmail
{
@ -46,11 +45,10 @@ class SendEmail
$this->reminder_template = $this->quote->calculateTemplate('quote');
}
$mo = new MailObject();
$this->quote->service()->markSent()->save();
$this->quote->invitations->each(function ($invitation) use ($mo) {
$this->quote->invitations->each(function ($invitation) {
if (! $invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template);

View File

@ -0,0 +1,43 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Recurring;
use App\Models\RecurringInvoice;
use App\Services\AbstractService;
class IncreasePrice extends AbstractService
{
public function __construct(public RecurringInvoice $recurring_invoice, public float $percentage)
{
}
public function run()
{
$line_items = $this->recurring_invoice->line_items;
foreach ($line_items as $key => $line_item) {
$line_items[$key]->cost = $line_item->cost * (1 + round(($this->percentage / 100), 2));
}
$this->recurring_invoice->line_items = $line_items;
$this->recurring_invoice->calc()->getInvoice()->save();
}
}

View File

@ -11,18 +11,22 @@
namespace App\Services\Recurring;
use App\Jobs\RecurringInvoice\SendRecurring;
use App\Jobs\Util\UnlinkFile;
use App\Models\RecurringInvoice;
use Illuminate\Support\Carbon;
use App\Models\RecurringExpense;
use App\Models\RecurringInvoice;
use App\Services\Recurring\ApplyNumber;
use App\Services\Recurring\UpdatePrice;
use App\Services\Recurring\GetInvoicePdf;
use App\Services\Recurring\IncreasePrice;
use App\Jobs\RecurringInvoice\SendRecurring;
use App\Services\Recurring\CreateRecurringInvitations;
class RecurringService
{
protected $recurring_entity;
public function __construct($recurring_entity)
public function __construct(public RecurringInvoice | RecurringExpense $recurring_entity)
{
$this->recurring_entity = $recurring_entity;
}
//set schedules - update next_send_dates
@ -135,6 +139,21 @@ class RecurringService
{
return $this;
}
public function increasePrice(float $percentage)
{
(new IncreasePrice($this->recurring_entity, $percentage))->run();
return $this;
}
public function updatePrice()
{
(new UpdatePrice($this->recurring_entity))->run();
return $this;
}
public function save()
{

View File

@ -0,0 +1,50 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Recurring;
use App\Models\Product;
use App\Models\RecurringInvoice;
use App\Services\AbstractService;
class UpdatePrice extends AbstractService
{
public function __construct(public RecurringInvoice $recurring_invoice)
{
}
public function run()
{
$line_items = $this->recurring_invoice->line_items;
foreach($line_items as $key => $line_item)
{
$product = Product::where('company_id', $this->recurring_invoice->company_id)
->where('product_key', $line_item->product_key)
->where('is_deleted', 0)
->first();
if($product){
$line_items[$key]->cost = $product->cost;
}
}
$this->recurring_invoice->line_items = $line_items;
$this->recurring_invoice->calc()->getInvoice()->save();
}
}

View File

@ -80,7 +80,11 @@ class CompanyUserTransformer extends EntityTransformer
public function includeToken(CompanyUser $company_user)
{
$token = $company_user->tokens()->where('company_id', $company_user->company_id)->where('user_id', $company_user->user_id)->first();
$token = $company_user->tokens()
->where('company_id', $company_user->company_id)
->where('user_id', $company_user->user_id)
->where('is_system', 1)
->first();
$transformer = new CompanyTokenTransformer($this->serializer);

View File

@ -683,6 +683,8 @@ class HtmlEngine
$data['labels'][$key.'_label'] = $value['label'];
}
// nlog($data);
return $data;
}
@ -762,9 +764,6 @@ class HtmlEngine
if ($country) {
return $country->iso_3166_2;
}
// if ($country) {
// return ctrans('texts.country_' . $country->iso_3166_2);
// }
return ' ';
}

View File

@ -511,7 +511,7 @@ class VendorHtmlEngine
$data['values'][$key] = $value['value'];
$data['labels'][$key.'_label'] = $value['label'];
}
nlog($data);
return $data;
}

30
composer.lock generated
View File

@ -2171,16 +2171,16 @@
},
{
"name": "google/apiclient-services",
"version": "v0.289.0",
"version": "v0.290.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "937f83a927db2d09db7eebb69ce2ac4114559bd7"
"reference": "df7e6cbab08f60509b3f360d8286c194ad2930e2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/937f83a927db2d09db7eebb69ce2ac4114559bd7",
"reference": "937f83a927db2d09db7eebb69ce2ac4114559bd7",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/df7e6cbab08f60509b3f360d8286c194ad2930e2",
"reference": "df7e6cbab08f60509b3f360d8286c194ad2930e2",
"shasum": ""
},
"require": {
@ -2209,9 +2209,9 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.289.0"
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.290.0"
},
"time": "2023-02-26T01:10:11+00:00"
"time": "2023-03-01T17:20:18+00:00"
},
{
"name": "google/auth",
@ -14019,16 +14019,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.14.4",
"version": "v3.15.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "1b3d9dba63d93b8a202c31e824748218781eae6b"
"reference": "7306744c63e9cc1337894252b4eec4920c38b053"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/1b3d9dba63d93b8a202c31e824748218781eae6b",
"reference": "1b3d9dba63d93b8a202c31e824748218781eae6b",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7306744c63e9cc1337894252b4eec4920c38b053",
"reference": "7306744c63e9cc1337894252b4eec4920c38b053",
"shasum": ""
},
"require": {
@ -14095,9 +14095,15 @@
}
],
"description": "A tool to automatically fix PHP code style",
"keywords": [
"Static code analysis",
"fixer",
"standards",
"static analysis"
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.14.4"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.15.0"
},
"funding": [
{
@ -14105,7 +14111,7 @@
"type": "github"
}
],
"time": "2023-02-09T21:49:13+00:00"
"time": "2023-03-12T22:44:55+00:00"
},
{
"name": "hamcrest/hamcrest-php",

View File

@ -1,5 +1,10 @@
<?php
use Imdhemy\Purchases\Events\AppStore\DidRenew;
use App\Listeners\Subscription\AppStoreRenewSubscription;
use App\Listeners\Subscription\PlayStoreRenewSubscription;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRenewed;
return [
/*
|--------------------------------------------------------------------------

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.89',
'app_tag' => '5.5.89',
'app_version' => '5.5.90',
'app_tag' => '5.5.90',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -1,74 +0,0 @@
<?php
use Imdhemy\Purchases\Events\AppStore\Cancel;
use Imdhemy\Purchases\Events\AppStore\Refund;
use Imdhemy\Purchases\Events\AppStore\Revoke;
use Imdhemy\Purchases\Events\AppStore\DidRenew;
use Imdhemy\Purchases\Events\AppStore\DidRecover;
use Imdhemy\Purchases\Events\AppStore\InitialBuy;
use Imdhemy\Purchases\Events\AppStore\DidFailToRenew;
use App\Listeners\Subscription\AppStoreRenewSubscription;
use Imdhemy\Purchases\Events\AppStore\InteractiveRenewal;
use App\Listeners\Subscription\PlayStoreRenewSubscription;
use Imdhemy\Purchases\Events\AppStore\DidChangeRenewalPref;
use Imdhemy\Purchases\Events\AppStore\PriceIncreaseConsent;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionOnHold;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionPaused;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionExpired;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRenewed;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRevoked;
use Imdhemy\Purchases\Events\AppStore\DidChangeRenewalStatus;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionCanceled;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionDeferred;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionPurchased;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRecovered;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRestarted;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionInGracePeriod;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionPauseScheduleChanged;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionPriceChangeConfirmed;
return [
'routing' => [],
'google_play_package_name' => env('GOOGLE_PLAY_PACKAGE_NAME', 'com.invoiceninja.app'),
'appstore_password' => env('APPSTORE_PASSWORD', ''),
'eventListeners' => [
/**
* --------------------------------------------------------
* Google Play Events
* --------------------------------------------------------
*/
SubscriptionPurchased::class => [],
SubscriptionRenewed::class => [PlayStoreRenewSubscription::class],
SubscriptionInGracePeriod::class => [],
SubscriptionExpired::class => [],
SubscriptionCanceled::class => [],
SubscriptionPaused::class => [],
SubscriptionRestarted::class => [],
SubscriptionDeferred::class => [],
SubscriptionRevoked::class => [],
SubscriptionOnHold::class => [],
SubscriptionRecovered::class => [],
SubscriptionPauseScheduleChanged::class => [],
SubscriptionPriceChangeConfirmed::class => [],
/**
* --------------------------------------------------------
* Appstore Events
* --------------------------------------------------------
*/
Cancel::class => [],
DidChangeRenewalPref::class => [],
DidChangeRenewalStatus::class => [],
DidFailToRenew::class => [],
DidRecover::class => [],
DidRenew::class => [AppStoreRenewSubscription::class],
InitialBuy::class => [],
InteractiveRenewal::class => [],
PriceIncreaseConsent::class => [],
Refund::class => [],
Revoke::class => [],
],
];

View File

@ -118,6 +118,9 @@
background-color: {{ $primary_color }};
}
.logo {
}
</style>
<!--[if gte mso 9]>

View File

@ -18,6 +18,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
use Illuminate\Validation\ValidationException;
/**
* @test
@ -40,6 +41,34 @@ class InvoiceEmailTest extends TestCase
Model::reguard();
$this->makeTestData();
$this->withoutExceptionHandling();
}
public function test_cc_email_implementation()
{
$data = [
'template' => 'email_template_invoice',
'entity' => 'invoice',
'entity_id' => $this->invoice->hashed_id,
'cc_email' => 'jj@gmail.com'
];
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/emails', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(200);
}
public function test_initial_email_send_emails()

View File

@ -13,12 +13,15 @@ namespace Tests\Feature;
use App\Factory\InvoiceItemFactory;
use App\Factory\InvoiceToRecurringInvoiceFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Factory\RecurringInvoiceToInvoiceFactory;
use App\Jobs\RecurringInvoice\UpdateRecurring;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\RecurringInvoice;
use App\Utils\Helpers;
use App\Utils\Traits\MakesHash;
use App\Models\Product;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
@ -53,6 +56,194 @@ class RecurringInvoiceTest extends TestCase
$this->makeTestData();
}
public function testBulkIncreasePriceWithJob()
{
$recurring_invoice = RecurringInvoiceFactory::create($this->company->id, $this->user->id);
$recurring_invoice->client_id = $this->client->id;
$line_items[] = [
'product_key' => 'pink',
'notes' => 'test',
'cost' => 10,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
];
$recurring_invoice->line_items = $line_items;
$recurring_invoice->calc()->getInvoice()->service()->start()->save()->fresh();
(new UpdateRecurring([$recurring_invoice->id], $this->company, $this->user, 'increase_prices', 10))->handle();
$recurring_invoice->refresh();
$this->assertEquals(11, $recurring_invoice->amount);
}
public function testBulkUpdateWithJob()
{
$p = Product::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'cost' => 20,
'product_key' => 'pink',
]);
$recurring_invoice = RecurringInvoiceFactory::create($this->company->id, $this->user->id);
$recurring_invoice->client_id = $this->client->id;
$line_items[] = [
'product_key' => 'pink',
'notes' => 'test',
'cost' => 10,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
];
$recurring_invoice->line_items = $line_items;
$recurring_invoice->calc()->getInvoice()->service()->start()->save()->fresh();
(new UpdateRecurring([$recurring_invoice->id], $this->company, $this->user, 'update_prices'))->handle();
$recurring_invoice->refresh();
$this->assertEquals(20, $recurring_invoice->amount);
}
public function testBulkUpdatePrices()
{
$p = Product::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'cost' => 10,
'product_key' => 'pink',
]);
$recurring_invoice = RecurringInvoiceFactory::create($this->company->id, $this->user->id);
$recurring_invoice->client_id = $this->client->id;
$line_items[] = [
'product_key' => 'pink',
'notes' => 'test',
'cost' => 10,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
];
$recurring_invoice->line_items = $line_items;
$recurring_invoice->calc()->getInvoice()->service()->start()->save()->fresh();
$this->assertEquals(10, $recurring_invoice->amount);
$p->cost = 20;
$p->save();
$recurring_invoice->service()->updatePrice();
$recurring_invoice->refresh();
$this->assertEquals(20, $recurring_invoice->amount);
$recurring_invoice->service()->increasePrice(10);
$recurring_invoice->refresh();
$this->assertEquals(22, $recurring_invoice->amount);
}
public function testBulkUpdateMultiPrices()
{
$p1 = Product::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'cost' => 10,
'product_key' => 'pink',
]);
$p2 = Product::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'cost' => 20,
'product_key' => 'floyd',
]);
$recurring_invoice = RecurringInvoiceFactory::create($this->company->id, $this->user->id);
$recurring_invoice->client_id = $this->client->id;
$line_items[] = [
'product_key' => 'floyd',
'notes' => 'test',
'cost' => 20,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
];
$line_items[] = [
'product_key' => 'pink',
'notes' => 'test',
'cost' => 10,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
];
$recurring_invoice->line_items = $line_items;
$recurring_invoice->calc()->getInvoice()->service()->start()->save()->fresh();
$this->assertEquals(30, $recurring_invoice->amount);
$p1->cost = 20;
$p1->save();
$p2->cost = 40;
$p2->save();
$recurring_invoice->service()->updatePrice();
$recurring_invoice->refresh();
$this->assertEquals(60, $recurring_invoice->amount);
$recurring_invoice->service()->increasePrice(10);
$recurring_invoice->refresh();
$this->assertEquals(66, $recurring_invoice->amount);
$recurring_invoice->service()->increasePrice(1);
$recurring_invoice->refresh();
$this->assertEquals(66.66, $recurring_invoice->amount);
}
public function testRecurringGetStatus()
{
$response = $this->withHeaders([

View File

@ -219,6 +219,7 @@ trait MockAccountData
$this->account = Account::factory()->create([
'hosted_client_count' => 1000000,
'hosted_company_count' => 1000000,
'account_sms_verified' => true,
]);
$this->account->num_users = 3;