diff --git a/VERSION.txt b/VERSION.txt index 030ab30ddf..83e2d80cab 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.6.28 \ No newline at end of file +5.6.29 \ No newline at end of file diff --git a/app/Events/Vendor/VendorContactLoggedIn.php b/app/Events/Vendor/VendorContactLoggedIn.php new file mode 100644 index 0000000000..2422af1023 --- /dev/null +++ b/app/Events/Vendor/VendorContactLoggedIn.php @@ -0,0 +1,45 @@ +put('current_balance', $properties['currentBalance']['amount'] ?? 0); + $properties->put('account_currency', $properties['currentBalance']['currency'] ?? 0); + + return $properties; + } +} \ No newline at end of file diff --git a/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php b/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php index 2043c865d2..19ff1974c6 100644 --- a/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php +++ b/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php @@ -64,6 +64,7 @@ use App\Helpers\Bank\AccountTransformerInterface; class AccountTransformer implements AccountTransformerInterface { + public function transform($yodlee_account) { $data = []; @@ -93,13 +94,54 @@ class AccountTransformer implements AccountTransformerInterface $account_currency = $account->balance->currency ?? ''; } + $account_status = $account->accountStatus; + + if(property_exists($account, 'dataset')){ + $dataset = $account->dataset[0]; + $status = false; + $update = false; + + match($dataset->additionalStatus ?? ''){ + 'LOGIN_IN_PROGRESS' => $status = 'Data retrieval in progress.', + 'USER_INPUT_REQUIRED' => $status = 'Please reconnect your account, authentication required.', + 'LOGIN_SUCCESS' => $status = 'Data retrieval in progress', + 'ACCOUNT_SUMMARY_RETRIEVED' => $status = 'Account summary retrieval in progress.', + 'NEVER_INITIATED' => $status = 'Upstream working on connecting to your account.', + 'LOGIN_FAILED' => $status = 'Authentication failed, please try reauthenticating.', + 'REQUEST_TIME_OUT' => $status = 'Timeout encountered retrieving data.', + 'DATA_RETRIEVAL_FAILED' => $status = 'Login successful, but data retrieval failed.', + 'PARTIAL_DATA_RETRIEVED' => $status = 'Partial data update failed.', + 'PARTIAL_DATA_RETRIEVED_REM_SCHED' => $status = 'Partial data update failed.', + 'SUCCESS' => $status = 'All accounts added or updated successfully.', + default => $status = false + }; + + if($status){ + $account_status = $status; + } + + match($dataset->updateEligibility ?? ''){ + 'ALLOW_UPDATE' => $update = 'Account connection stable.', + 'ALLOW_UPDATE_WITH_CREDENTIALS' => $update = 'Please reconnect your account with updated credentials.', + 'DISALLOW_UPDATE' => $update = 'Update not available due to technical issues.', + default => $update = false, + }; + + if($status && $update){ + $account_status = $status . ' - ' . $update; + } + elseif($update){ + $account_status = $update; + } + + } return [ 'id' => $account->id, 'account_type' => $account->CONTAINER, // 'account_name' => $account->accountName, 'account_name' => property_exists($account, 'accountName') ? $account->accountName : $account->nickname, - 'account_status' => $account->accountStatus, + 'account_status' => $account_status, 'account_number' => property_exists($account, 'accountNumber') ? '**** ' . substr($account?->accountNumber, -7) : '', 'provider_account_id' => $account->providerAccountId, 'provider_id' => $account->providerId, diff --git a/app/Helpers/Bank/Yodlee/Yodlee.php b/app/Helpers/Bank/Yodlee/Yodlee.php index fb506981fb..f8294becd9 100644 --- a/app/Helpers/Bank/Yodlee/Yodlee.php +++ b/app/Helpers/Bank/Yodlee/Yodlee.php @@ -297,4 +297,71 @@ class Yodlee 'secret' => $this->client_secret, ]; } + + /** + * updateEligibility + * + * ALLOW_UPDATE + * ALLOW_UPDATE_WITH_CREDENTIALS + * DISALLOW_UPDATE + */ + + /** + * additionalStatus + * + * LOGIN_IN_PROGRESS + * DATA_RETRIEVAL_IN_PROGRESS + * ACCT_SUMMARY_RECEIVED + * AVAILABLE_DATA_RETRIEVED + * PARTIAL_DATA_RETRIEVED + * DATA_RETRIEVAL_FAILED + * DATA_NOT_AVAILABLE + * ACCOUNT_LOCKED + * ADDL_AUTHENTICATION_REQUIRED + * BETA_SITE_DEV_IN_PROGRESS + * CREDENTIALS_UPDATE_NEEDED + * INCORRECT_CREDENTIALS + * PROPERTY_VALUE_NOT_AVAILABLE + * INVALID_ADDL_INFO_PROVIDED + * REQUEST_TIME_OUT + * SITE_BLOCKING_ERROR + * UNEXPECTED_SITE_ERROR + * SITE_NOT_SUPPORTED + * SITE_UNAVAILABLE + * TECH_ERROR + * USER_ACTION_NEEDED_AT_SITE + * SITE_SESSION_INVALIDATED + * NEW_AUTHENTICATION_REQUIRED + * DATASET_NOT_SUPPORTED + * ENROLLMENT_REQUIRED_FOR_DATASET + * CONSENT_REQUIRED + * CONSENT_EXPIRED + * CONSENT_REVOKED + * INCORRECT_OAUTH_TOKEN + * MIGRATION_IN_PROGRESS + */ + + /** + * IN_PROGRESS LOGIN_IN_PROGRESS Provider login is in progress. + * IN_PROGRESS USER_INPUT_REQUIRED Provider site requires MFA-based authentication and needs user input for login. + * IN_PROGRESS LOGIN_SUCCESS Provider login is successful. + * IN_PROGRESS ACCOUNT_SUMMARY_RETRIEVED Account summary info may not have the complete info of accounts that are available in the provider site. This depends on the sites behaviour. Account summary info may not be available at all times. + * FAILED NEVER_INITIATED The add or update provider account was not triggered due to techincal reasons. This is a rare occurrence and usually resolves quickly. + * FAILED LOGIN_FAILED Provider login failed. + * FAILED REQUEST_TIME_OUT The process timed out. + * FAILED DATA_RETRIEVAL_FAILED All accounts under the provider account failed with same or different errors, though login was successful. + * FAILED No additional status or information will be provided when there are errors other than the ones listed above. + * PARTIAL_SUCCESS PARTIAL_DATA_RETRIEVED DATA_RETRIEVAL_FAILED_PARTIALLY One/few accounts data gathered and one/few accounts failed. + * PARTIAL_SUCCESS PARTIAL_DATA_RETRIEVED_REM_SCHED DATA_RETRIEVAL_FAILED_PARTIALLY One/few accounts data gathered One/few accounts failed + * SUCCESS All accounts under the provider was added or updated successfully. + */ + + /** + * updateEligibility + * + * ALLOW_UPDATE The status indicates that the account is eligible for the next update and applies to both MFA and non-MFA accounts. For MFA-based accounts, the user may have to provide the MFA details during account refresh. + * ALLOW_UPDATE_WITH_CREDENTIALS The status indicates updating or refreshing the account by directing the user to edit the provided credentials. + * DISALLOW_UPDATE The status indicates the account is not eligible for the update or refresh process due to a site issue or a technical error. + */ + } diff --git a/app/Http/Controllers/Bank/YodleeController.php b/app/Http/Controllers/Bank/YodleeController.php index 8f4c1691af..34bfa509c6 100644 --- a/app/Http/Controllers/Bank/YodleeController.php +++ b/app/Http/Controllers/Bank/YodleeController.php @@ -11,12 +11,14 @@ namespace App\Http\Controllers\Bank; +use App\Helpers\Bank\Yodlee\DTO\AccountSummary; +use Illuminate\Http\Request; +use App\Models\BankIntegration; use App\Helpers\Bank\Yodlee\Yodlee; use App\Http\Controllers\BaseController; -use App\Http\Requests\Yodlee\YodleeAuthRequest; use App\Jobs\Bank\ProcessBankTransactions; -use App\Models\BankIntegration; -use Illuminate\Http\Request; +use App\Http\Requests\Yodlee\YodleeAuthRequest; +use App\Http\Requests\Yodlee\YodleeAdminRequest; class YodleeController extends BaseController { @@ -277,4 +279,27 @@ class YodleeController extends BaseController // return response()->json(['message' => 'Unauthorized'], 403); } + + public function accountStatus(YodleeAdminRequest $request, $account_number) + { + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $bank_integration = BankIntegration::query() + ->withTrashed() + ->where('company_id', $user->company()->id) + ->where('account_id', $account_number) + ->exists(); + + if(!$bank_integration) + return response()->json(['message' => 'Account does not exist.'], 400); + + $yodlee = new Yodlee($user->account->bank_integration_account_id); + + $summary = $yodlee->getAccountSummary($account_number); + + $transformed_summary = AccountSummary::from($summary[0]); + + return response()->json($transformed_summary, 200); + } } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 59c6d5c576..060be29f0d 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -11,30 +11,31 @@ namespace App\Http\Controllers; -use App\Events\Client\ClientWasCreated; -use App\Events\Client\ClientWasUpdated; +use App\Utils\Ninja; +use App\Models\Client; +use App\Models\Account; +use Illuminate\Http\Response; use App\Factory\ClientFactory; use App\Filters\ClientFilters; +use App\Utils\Traits\MakesHash; +use App\Utils\Traits\Uploadable; +use App\Utils\Traits\BulkOptions; +use App\Jobs\Client\UpdateTaxData; +use App\Utils\Traits\SavesDocuments; +use App\Repositories\ClientRepository; +use App\Events\Client\ClientWasCreated; +use App\Events\Client\ClientWasUpdated; +use App\Transformers\ClientTransformer; +use Illuminate\Support\Facades\Storage; use App\Http\Requests\Client\BulkClientRequest; -use App\Http\Requests\Client\CreateClientRequest; -use App\Http\Requests\Client\DestroyClientRequest; use App\Http\Requests\Client\EditClientRequest; -use App\Http\Requests\Client\PurgeClientRequest; use App\Http\Requests\Client\ShowClientRequest; +use App\Http\Requests\Client\PurgeClientRequest; use App\Http\Requests\Client\StoreClientRequest; +use App\Http\Requests\Client\CreateClientRequest; use App\Http\Requests\Client\UpdateClientRequest; use App\Http\Requests\Client\UploadClientRequest; -use App\Models\Account; -use App\Models\Client; -use App\Repositories\ClientRepository; -use App\Transformers\ClientTransformer; -use App\Utils\Ninja; -use App\Utils\Traits\BulkOptions; -use App\Utils\Traits\MakesHash; -use App\Utils\Traits\SavesDocuments; -use App\Utils\Traits\Uploadable; -use Illuminate\Http\Response; -use Illuminate\Support\Facades\Storage; +use App\Http\Requests\Client\DestroyClientRequest; /** * Class ClientController. @@ -285,4 +286,18 @@ class ClientController extends BaseController return $this->itemResponse($merged_client); } + + /** + * Updates the client's tax data + * + * @param PurgeClientRequest $request + * @param Client $client + * @return \Illuminate\Http\JsonResponse + */ + public function updateTaxData(PurgeClientRequest $request, Client $client) + { + (new UpdateTaxData($client, $client->company))->handle(); + + return $this->itemResponse($client->fresh()); + } } diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index ccb6f8f53f..e5007cca9a 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -11,26 +11,27 @@ namespace App\Http\Controllers\ClientPortal; -use App\Events\Credit\CreditWasViewed; -use App\Events\Invoice\InvoiceWasViewed; -use App\Events\Misc\InvitationWasViewed; +use App\Utils\Ninja; +use App\Models\Client; +use App\Models\Payment; +use Illuminate\Support\Str; +use Illuminate\Http\Request; +use App\Models\ClientContact; +use App\Models\QuoteInvitation; +use App\Utils\Traits\MakesHash; +use App\Models\CreditInvitation; +use App\Utils\Traits\MakesDates; +use App\Jobs\Entity\CreateRawPdf; +use App\Models\InvoiceInvitation; use App\Events\Quote\QuoteWasViewed; use App\Http\Controllers\Controller; -use App\Jobs\Entity\CreateRawPdf; -use App\Models\Client; -use App\Models\ClientContact; -use App\Models\CreditInvitation; -use App\Models\InvoiceInvitation; -use App\Models\Payment; -use App\Models\PurchaseOrderInvitation; -use App\Models\QuoteInvitation; -use App\Services\ClientPortal\InstantPayment; -use App\Utils\Ninja; -use App\Utils\Traits\MakesDates; -use App\Utils\Traits\MakesHash; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Str; +use App\Events\Credit\CreditWasViewed; +use App\Events\Contact\ContactLoggedIn; +use App\Models\PurchaseOrderInvitation; +use App\Events\Invoice\InvoiceWasViewed; +use App\Events\Misc\InvitationWasViewed; +use App\Services\ClientPortal\InstantPayment; /** * Class InvitationController. @@ -94,6 +95,7 @@ class InvitationController extends Controller } $client_contact = $invitation->contact; + event(new ContactLoggedIn($client_contact, $client_contact->company, Ninja::eventVars())); if (empty($client_contact->email)) { $client_contact->email = Str::random(15) . "@example.com"; diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 3741c61359..4347f6a28b 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -11,38 +11,39 @@ namespace App\Http\Controllers; -use App\DataMapper\Analytics\AccountDeleted; -use App\DataMapper\CompanySettings; -use App\Http\Requests\Company\CreateCompanyRequest; -use App\Http\Requests\Company\DefaultCompanyRequest; -use App\Http\Requests\Company\DestroyCompanyRequest; -use App\Http\Requests\Company\EditCompanyRequest; -use App\Http\Requests\Company\ShowCompanyRequest; -use App\Http\Requests\Company\StoreCompanyRequest; -use App\Http\Requests\Company\UpdateCompanyRequest; -use App\Http\Requests\Company\UploadCompanyRequest; -use App\Jobs\Company\CreateCompany; -use App\Jobs\Company\CreateCompanyPaymentTerms; -use App\Jobs\Company\CreateCompanyTaskStatuses; -use App\Jobs\Company\CreateCompanyToken; -use App\Jobs\Mail\NinjaMailerJob; -use App\Jobs\Mail\NinjaMailerObject; -use App\Mail\Company\CompanyDeleted; +use Str; +use App\Utils\Ninja; use App\Models\Account; use App\Models\Company; use App\Models\CompanyUser; -use App\Repositories\CompanyRepository; -use App\Transformers\CompanyTransformer; -use App\Transformers\CompanyUserTransformer; -use App\Utils\Ninja; -use App\Utils\Traits\MakesHash; -use App\Utils\Traits\SavesDocuments; -use App\Utils\Traits\Uploadable; -use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Response; -use Illuminate\Support\Facades\Storage; -use Str; +use App\Utils\Traits\MakesHash; +use App\Utils\Traits\Uploadable; +use App\Jobs\Mail\NinjaMailerJob; +use App\DataMapper\CompanySettings; +use App\Jobs\Company\CreateCompany; +use App\Jobs\Company\CompanyTaxRate; +use App\Jobs\Mail\NinjaMailerObject; +use App\Mail\Company\CompanyDeleted; +use App\Utils\Traits\SavesDocuments; use Turbo124\Beacon\Facades\LightLogs; +use App\Repositories\CompanyRepository; +use Illuminate\Support\Facades\Storage; +use App\Jobs\Company\CreateCompanyToken; +use App\Transformers\CompanyTransformer; +use App\DataMapper\Analytics\AccountDeleted; +use App\Transformers\CompanyUserTransformer; +use Illuminate\Foundation\Bus\DispatchesJobs; +use App\Jobs\Company\CreateCompanyPaymentTerms; +use App\Jobs\Company\CreateCompanyTaskStatuses; +use App\Http\Requests\Company\EditCompanyRequest; +use App\Http\Requests\Company\ShowCompanyRequest; +use App\Http\Requests\Company\StoreCompanyRequest; +use App\Http\Requests\Company\CreateCompanyRequest; +use App\Http\Requests\Company\UpdateCompanyRequest; +use App\Http\Requests\Company\UploadCompanyRequest; +use App\Http\Requests\Company\DefaultCompanyRequest; +use App\Http\Requests\Company\DestroyCompanyRequest; /** * Class CompanyController. @@ -679,4 +680,21 @@ class CompanyController extends BaseController return $this->itemResponse($company->fresh()); } + + public function updateOriginTaxData(DefaultCompanyRequest $request, Company $company) + { + + if($company->settings->country_id == "840" && !$company?->account->isFreeHostedClient()) + { + try { + (new CompanyTaxRate($company))->handle(); + } catch(\Exception $e) { + return response()->json(['message' => 'There was a problem updating the tax rates. Please try again.'], 400); + } + } + else + return response()->json(['message' => 'Tax configuration not available due to settings / plan restriction.'], 400); + + return $this->itemResponse($company->fresh()); + } } diff --git a/app/Http/Middleware/VendorContactKeyLogin.php b/app/Http/Middleware/VendorContactKeyLogin.php index 352d96560c..5b569bbded 100644 --- a/app/Http/Middleware/VendorContactKeyLogin.php +++ b/app/Http/Middleware/VendorContactKeyLogin.php @@ -11,14 +11,16 @@ namespace App\Http\Middleware; -use App\Libraries\MultiDB; -use App\Models\Vendor; -use App\Models\VendorContact; use Auth; use Closure; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Cache; +use App\Utils\Ninja; +use App\Models\Vendor; +use App\Libraries\MultiDB; use Illuminate\Support\Str; +use Illuminate\Http\Request; +use App\Models\VendorContact; +use Illuminate\Support\Facades\Cache; +use App\Events\Vednor\VendorContactLoggedIn; class VendorContactKeyLogin { @@ -56,6 +58,8 @@ class VendorContactKeyLogin $vendor_contact->save(); auth()->guard('vendor')->loginUsingId($vendor_contact->id, true); + + event(new VendorContactLoggedIn($vendor_contact, $vendor_contact->company, Ninja::eventVars())); if ($request->query('redirect') && ! empty($request->query('redirect'))) { return redirect()->to($request->query('redirect')); @@ -72,7 +76,7 @@ class VendorContactKeyLogin $vendor_contact->save(); auth()->guard('vendor')->loginUsingId($vendor_contact->id, true); - + event(new VendorContactLoggedIn($vendor_contact, $vendor_contact->company, Ninja::eventVars())); if ($request->query('next')) { return redirect()->to($request->query('next')); } diff --git a/app/Http/Middleware/VendorLocale.php b/app/Http/Middleware/VendorLocale.php index a64fc1ec41..abca08b9e3 100644 --- a/app/Http/Middleware/VendorLocale.php +++ b/app/Http/Middleware/VendorLocale.php @@ -36,7 +36,7 @@ class VendorLocale $locale = $request->input('lang'); App::setLocale($locale); } elseif (auth()->guard('vendor')->user()) { - App::setLocale(auth()->guard('vendor')->user()->company->locale()); + App::setLocale(auth()->guard('vendor')->user()->vendor->locale()); } elseif (auth()->user()) { try { App::setLocale(auth()->user()->company()->getLocale()); diff --git a/app/Http/Requests/Company/DefaultCompanyRequest.php b/app/Http/Requests/Company/DefaultCompanyRequest.php index a2de144804..342b71fe7e 100644 --- a/app/Http/Requests/Company/DefaultCompanyRequest.php +++ b/app/Http/Requests/Company/DefaultCompanyRequest.php @@ -22,7 +22,10 @@ class DefaultCompanyRequest extends Request */ public function authorize() : bool { - return auth()->user()->isAdmin(); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->isAdmin(); } public function rules() diff --git a/app/Http/Requests/Payment/StorePaymentRequest.php b/app/Http/Requests/Payment/StorePaymentRequest.php index b178717caa..97736fda7a 100644 --- a/app/Http/Requests/Payment/StorePaymentRequest.php +++ b/app/Http/Requests/Payment/StorePaymentRequest.php @@ -85,8 +85,6 @@ class StorePaymentRequest extends Request $input['amount'] = $invoices_total - $credits_total; } - // $input['is_manual'] = true; - if (! isset($input['date'])) { $input['date'] = now()->addSeconds(auth()->user()->company()->timezone()->utc_offset)->format('Y-m-d'); } diff --git a/app/Http/Requests/Vendor/StoreVendorRequest.php b/app/Http/Requests/Vendor/StoreVendorRequest.php index 777930dd70..2de5edeca0 100644 --- a/app/Http/Requests/Vendor/StoreVendorRequest.php +++ b/app/Http/Requests/Vendor/StoreVendorRequest.php @@ -26,17 +26,23 @@ class StoreVendorRequest extends Request */ public function authorize() : bool { - return auth()->user()->can('create', Vendor::class); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('create', Vendor::class); } public function rules() { + /** @var \App\Models\User $user */ + $user = auth()->user(); + $rules = []; $rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email'; if (isset($this->number)) { - $rules['number'] = Rule::unique('vendors')->where('company_id', auth()->user()->company()->id); + $rules['number'] = Rule::unique('vendors')->where('company_id', $user->company()->id); } $rules['currency_id'] = 'bail|required|exists:currencies,id'; @@ -53,15 +59,20 @@ class StoreVendorRequest extends Request $rules['file'] = $this->file_validation; } + $rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id'; + return $rules; } public function prepareForValidation() { + /** @var \App\Models\User $user */ + $user = auth()->user(); + $input = $this->all(); if (!array_key_exists('currency_id', $input) || empty($input['currency_id'])) { - $input['currency_id'] = auth()->user()->company()->settings->currency_id; + $input['currency_id'] = $user->company()->settings->currency_id; } $input = $this->decodePrimaryKeys($input); diff --git a/app/Http/Requests/Vendor/UpdateVendorRequest.php b/app/Http/Requests/Vendor/UpdateVendorRequest.php index 2010b38d1d..867b4541c0 100644 --- a/app/Http/Requests/Vendor/UpdateVendorRequest.php +++ b/app/Http/Requests/Vendor/UpdateVendorRequest.php @@ -28,17 +28,21 @@ class UpdateVendorRequest extends Request */ public function authorize() : bool { - return auth()->user()->can('edit', $this->vendor); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('edit', $this->vendor); } public function rules() { - /* Ensure we have a client name, and that all emails are unique*/ - + /** @var \App\Models\User $user */ + $user = auth()->user(); + $rules['country_id'] = 'integer'; if ($this->number) { - $rules['number'] = Rule::unique('vendors')->where('company_id', auth()->user()->company()->id)->ignore($this->vendor->id); + $rules['number'] = Rule::unique('vendors')->where('company_id', $user->company()->id)->ignore($this->vendor->id); } $rules['contacts.*.email'] = 'nullable|distinct'; @@ -56,6 +60,8 @@ class UpdateVendorRequest extends Request $rules['file'] = $this->file_validation; } + $rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id'; + return $rules; } diff --git a/app/Http/Requests/Yodlee/YodleeAuthRequest.php b/app/Http/Requests/Yodlee/YodleeAuthRequest.php index ff267c5f98..942139a2e2 100644 --- a/app/Http/Requests/Yodlee/YodleeAuthRequest.php +++ b/app/Http/Requests/Yodlee/YodleeAuthRequest.php @@ -39,6 +39,7 @@ class YodleeAuthRequest extends Request return []; } + /** @var $token */ public function getTokenContent() { if ($this->state) { diff --git a/app/Http/ValidationRules/Credit/ValidCreditsRules.php b/app/Http/ValidationRules/Credit/ValidCreditsRules.php index b0892da733..d02097df21 100644 --- a/app/Http/ValidationRules/Credit/ValidCreditsRules.php +++ b/app/Http/ValidationRules/Credit/ValidCreditsRules.php @@ -64,7 +64,6 @@ class ValidCreditsRules implements Rule foreach ($this->input['credits'] as $credit) { $unique_array[] = $credit['credit_id']; - // $cred = Credit::find($this->decodePrimaryKey($credit['credit_id'])); $cred = $cred_collection->firstWhere('id', $credit['credit_id']); if (! $cred) { @@ -77,6 +76,11 @@ class ValidCreditsRules implements Rule return false; } + if($cred->status_id == Credit::STATUS_DRAFT){ + $cred->service()->markSent()->save(); + $cred = $cred->fresh(); + } + if($cred->balance < $credit['amount']) { $this->error_msg = ctrans('texts.insufficient_credit_balance'); return false; diff --git a/app/Jobs/Bank/ProcessBankTransactions.php b/app/Jobs/Bank/ProcessBankTransactions.php index c5af8fd49a..2015b19d74 100644 --- a/app/Jobs/Bank/ProcessBankTransactions.php +++ b/app/Jobs/Bank/ProcessBankTransactions.php @@ -109,8 +109,8 @@ class ProcessBankTransactions implements ShouldQueue $account = $at->transform($account_summary); if($account[0]['current_balance']) { - $this->bank_integration->balance = $account['current_balance']; - $this->bank_integration->currency = $account['account_currency']; + $this->bank_integration->balance = $account[0]['current_balance']; + $this->bank_integration->currency = $account[0]['account_currency']; $this->bank_integration->save(); } diff --git a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php index fe6029aa20..5c8432603a 100644 --- a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php +++ b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php @@ -102,7 +102,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue /* Init a new copy of the translator*/ $t = app('translator'); /* Set the locale*/ - App::setLocale($this->company->locale()); + App::setLocale($this->vendor->locale()); /* Set customized translations _NOW_ */ $t->replace(Ninja::transformTranslations($this->company->settings)); diff --git a/app/Listeners/Vendor/UpdateVendorContactLastLogin.php b/app/Listeners/Vendor/UpdateVendorContactLastLogin.php new file mode 100644 index 0000000000..272b7144b1 --- /dev/null +++ b/app/Listeners/Vendor/UpdateVendorContactLastLogin.php @@ -0,0 +1,45 @@ +company->db); + + $contact = $event->contact; + + $contact->last_login = now(); + $contact->vendor->last_login = now(); + + $contact->push(); + } +} diff --git a/app/Models/Client.php b/app/Models/Client.php index 570e497ebc..9f69bfc0d1 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -58,7 +58,7 @@ use Illuminate\Contracts\Translation\HasLocalePreference; * @property string|null $city * @property string|null $state * @property string|null $postal_code - * @property string|null $country_id + * @property int|null $country_id * @property string|null $custom_value1 * @property string|null $custom_value2 * @property string|null $custom_value3 diff --git a/app/Models/Company.php b/app/Models/Company.php index 5fb8a44777..0c8ac30a59 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -58,7 +58,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property string|null $portal_domain * @property int $enable_modules * @property object $custom_fields - * @property object $settings + * @property \App\DataMapper\CompanySettings $settings * @property string $slack_webhook_url * @property string $google_analytics_key * @property int|null $created_at diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 43ed415da6..d72061bad7 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -51,6 +51,8 @@ use Laracasts\Presenter\PresentableTrait; * @property string|null $vendor_hash * @property string|null $public_notes * @property string|null $id_number + * @property string|null $language_id + * @property int|null $last_login * @property-read \Illuminate\Database\Eloquent\Collection $activities * @property-read int|null $activities_count * @property-read \App\Models\User|null $assigned_user @@ -140,6 +142,7 @@ class Vendor extends BaseModel 'custom_value3', 'custom_value4', 'number', + 'language_id', ]; protected $casts = [ @@ -149,6 +152,7 @@ class Vendor extends BaseModel 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', + 'last_login' => 'timestamp', ]; protected $touches = []; @@ -169,12 +173,12 @@ class Vendor extends BaseModel return $this->hasMany(VendorContact::class)->where('is_primary', true); } - public function documents() + public function documents(): \Illuminate\Database\Eloquent\Relations\MorphMany { return $this->morphMany(Document::class, 'documentable'); } - public function assigned_user() + public function assigned_user(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed(); } @@ -211,12 +215,12 @@ class Vendor extends BaseModel return $this->company->timezone(); } - public function company() + public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(Company::class); } - public function user() + public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(User::class)->withTrashed(); } @@ -265,24 +269,29 @@ class Vendor extends BaseModel return $this->company->settings; } - public function purchase_order_filepath($invitation) + public function purchase_order_filepath($invitation): string { $contact_key = $invitation->contact->contact_key; return $this->company->company_key.'/'.$this->vendor_hash.'/'.$contact_key.'/purchase_orders/'; } - public function locale() + public function locale(): string { - return $this->company->locale(); + return $this->language ? $this->language->locale : $this->company->locale(); } - public function country() + public function language(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(Language::class); + } + + public function country(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(Country::class); } - public function date_format() + public function date_format(): string { return $this->company->date_format(); } diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php index 91201af7a3..2e2da09850 100644 --- a/app/Models/VendorContact.php +++ b/app/Models/VendorContact.php @@ -106,6 +106,7 @@ class VendorContact extends Authenticatable implements HasLocalePreference 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', + 'last_login' => 'timestamp', ]; protected $fillable = [ diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 5e8f8d6087..d83f5602c8 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -70,13 +70,13 @@ use App\Events\Quote\QuoteWasRestored; use App\Events\Client\ClientWasCreated; use App\Events\Client\ClientWasDeleted; use App\Events\Client\ClientWasUpdated; -use App\Events\Design\DesignWasDeleted; -use App\Events\Design\DesignWasUpdated; use App\Events\Contact\ContactLoggedIn; use App\Events\Credit\CreditWasCreated; use App\Events\Credit\CreditWasDeleted; use App\Events\Credit\CreditWasEmailed; use App\Events\Credit\CreditWasUpdated; +use App\Events\Design\DesignWasDeleted; +use App\Events\Design\DesignWasUpdated; use App\Events\Vendor\VendorWasCreated; use App\Events\Vendor\VendorWasDeleted; use App\Events\Vendor\VendorWasUpdated; @@ -85,10 +85,10 @@ use App\Observers\SubscriptionObserver; use Illuminate\Mail\Events\MessageSent; use App\Events\Client\ClientWasArchived; use App\Events\Client\ClientWasRestored; -use App\Events\Design\DesignWasRestored; use App\Events\Credit\CreditWasArchived; use App\Events\Credit\CreditWasRestored; use App\Events\Design\DesignWasArchived; +use App\Events\Design\DesignWasRestored; use App\Events\Invoice\InvoiceWasViewed; use App\Events\Misc\InvitationWasViewed; use App\Events\Payment\PaymentWasVoided; @@ -133,6 +133,7 @@ use App\Listeners\User\UpdateUserLastLogin; use App\Events\Document\DocumentWasArchived; use App\Events\Document\DocumentWasRestored; use App\Events\Invoice\InvoiceWasMarkedSent; +use App\Events\Vendor\VendorContactLoggedIn; use App\Listeners\Quote\QuoteViewedActivity; use App\Listeners\User\ArchivedUserActivity; use App\Listeners\User\RestoredUserActivity; @@ -220,6 +221,7 @@ use App\Listeners\Invoice\InvoiceEmailFailedActivity; use App\Events\PurchaseOrder\PurchaseOrderWasAccepted; use App\Events\PurchaseOrder\PurchaseOrderWasArchived; use App\Events\PurchaseOrder\PurchaseOrderWasRestored; +use App\Listeners\Vendor\UpdateVendorContactLastLogin; use App\Events\RecurringQuote\RecurringQuoteWasCreated; use App\Events\RecurringQuote\RecurringQuoteWasDeleted; use App\Events\RecurringQuote\RecurringQuoteWasUpdated; @@ -615,6 +617,9 @@ class EventServiceProvider extends ServiceProvider VendorWasUpdated::class => [ VendorUpdatedActivity::class, ], + VendorContactLoggedIn::class => [ + UpdateVendorContactLastLogin::class, + ], \SocialiteProviders\Manager\SocialiteWasCalled::class => [ // ... Manager won't register drivers that are not added to this listener. \SocialiteProviders\Apple\AppleExtendSocialite::class.'@handle', diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/Invoice/EInvoice/FacturaEInvoice.php index 9f97da9e45..700bad3602 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/Invoice/EInvoice/FacturaEInvoice.php @@ -218,8 +218,10 @@ class FacturaEInvoice extends AbstractService private function setPoNumber(): self { $po = $this->invoice->po_number ?? ''; + $transaction_reference = (isset($this->invoice->custom_value1) && strlen($this->invoice->custom_value1) > 2) ? substr($this->invoice->custom_value1, 0, 20) : null; + $contract_reference = (isset($this->invoice->custom_value2) && strlen($this->invoice->custom_value2) > 2) ? $this->invoice->custom_value2: null; - $this->fac->setReferences($po, substr($this->invoice->custom_value1, 0, 20), $this->invoice->custom_value2); + $this->fac->setReferences($po, $transaction_reference, $contract_reference); return $this; } @@ -242,6 +244,9 @@ class FacturaEInvoice extends AbstractService private function setBillingPeriod(): self { + if(!$this->invoice->custom_value3) + return $this; + try { if (\Carbon\Carbon::createFromFormat('Y-m-d', $this->invoice->custom_value3)->format('Y-m-d') === $this->invoice->custom_value3 && \Carbon\Carbon::createFromFormat('Y-m-d', $this->invoice->custom_value4)->format('Y-m-d') === $this->invoice->custom_value4 diff --git a/app/Transformers/VendorContactTransformer.php b/app/Transformers/VendorContactTransformer.php index d06996c010..39f4d60d0e 100644 --- a/app/Transformers/VendorContactTransformer.php +++ b/app/Transformers/VendorContactTransformer.php @@ -44,6 +44,7 @@ class VendorContactTransformer extends EntityTransformer 'custom_value3' => $vendor->custom_value3 ?: '', 'custom_value4' => $vendor->custom_value4 ?: '', 'link' => $vendor->getLoginLink(), + 'last_login' => 'timestamp', ]; } } diff --git a/app/Transformers/VendorTransformer.php b/app/Transformers/VendorTransformer.php index f2acf862af..bbf1bfcde7 100644 --- a/app/Transformers/VendorTransformer.php +++ b/app/Transformers/VendorTransformer.php @@ -103,6 +103,7 @@ class VendorTransformer extends EntityTransformer 'archived_at' => (int) $vendor->deleted_at, 'created_at' => (int) $vendor->created_at, 'number' => (string) $vendor->number ?: '', + 'language_id' => (string) $vendor->language_id ?: '', ]; } } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 621568d2d0..97b2e7ea46 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -35,16 +35,22 @@ class HtmlEngine use MakesHash; use DesignCalculator; + /** @var \App\Models\Invoice | \App\Models\Credit | \App\Models\RecurringInvoice | \App\Models\Quote $entity **/ public $entity; + /** @var \App\Models\CreditInvitation | CreditInvitation | \App\Models\RecurringInvoiceInvitation | \App\Models\QuoteInvitation $invitation **/ public $invitation; + /** @var \App\Models\Client $client */ public $client; + /** @var \App\Models\ClientContact $contact */ public $contact; + /** @var \App\Models\Company $company */ public $company; + /** @var \App\DataMapper\CompanySettings $settings **/ public $settings; public $entity_calc; @@ -53,6 +59,13 @@ class HtmlEngine private $helpers; + + /** + * __construct + * + * @param InvoiceInvitation | CreditInvitation | RecurringInvoiceInvitation | QuoteInvitation $invitation + * @return void + */ public function __construct($invitation) { $this->invitation = $invitation; @@ -150,7 +163,9 @@ class HtmlEngine $data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')]; $data['$payment_qrcode'] = ['value' => $this->invitation->getPaymentQrCode(), 'label' => ctrans('texts.pay_now')]; $data['$exchange_rate'] = ['value' => $this->entity->exchange_rate ?: ' ', 'label' => ctrans('texts.exchange_rate')]; - + $data['$triangular_tax'] = ['value' => ctrans('texts.triangular_tax'), 'label' => '']; + $data['$tax_info'] = ['value' => $this->taxLabel(), 'label' => '']; + if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') { $data['$entity'] = ['value' => ctrans('texts.invoice'), 'label' => ctrans('texts.invoice')]; $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; @@ -676,6 +691,26 @@ class HtmlEngine return $data; } + + /** + * Returns a localized string for tax compliance purposes + * + * @return string + */ + private function taxLabel(): string + { + $tax_label = ''; + + if (collect($this->entity->line_items)->contains('tax_id', \App\Models\Product::PRODUCT_TYPE_REVERSE_TAX)) { + $tax_label .= ctrans('texts.reverse_tax_info') . "
"; + } + + if((int)$this->client->country_id !== (int)$this->company->settings->country_id){ + $tax_label .= ctrans('texts.intracommunity_tax_info') . "
"; + } + + return $tax_label; + } private function getBalance() { diff --git a/app/Utils/TranslationHelper.php b/app/Utils/TranslationHelper.php index e401f806bb..15850f62e4 100644 --- a/app/Utils/TranslationHelper.php +++ b/app/Utils/TranslationHelper.php @@ -12,7 +12,7 @@ namespace App\Utils; use App\Models\PaymentTerm; -use Cache; +use \Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; class TranslationHelper diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php index fa0bff0b8d..eb1f595584 100644 --- a/app/Utils/VendorHtmlEngine.php +++ b/app/Utils/VendorHtmlEngine.php @@ -112,7 +112,7 @@ class VendorHtmlEngine App::forgetInstance('translator'); $t = app('translator'); - App::setLocale($this->company->locale()); + App::setLocale($this->vendor->locale()); $t->replace(Ninja::transformTranslations($this->settings)); $data = []; @@ -126,16 +126,16 @@ class VendorHtmlEngine $data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')]; $data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')]; $data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')]; - $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.date')]; + $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->vendor->locale()) ?: ' ', 'label' => ctrans('texts.date')]; - $data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.due_date')]; + $data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->vendor->locale()) ?: ' ', 'label' => ctrans('texts.due_date')]; - $data['$partial_due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; + $data['$partial_due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->vendor->locale()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; $data['$dueDate'] = &$data['$due_date']; $data['$purchase_order.due_date'] = &$data['$due_date']; - $data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.payment_due')]; + $data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->company->date_format(), $this->vendor->locale()) ?: ' ', 'label' => ctrans('texts.payment_due')]; $data['$purchase_order.po_number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.po_number')]; $data['$poNumber'] = &$data['$purchase_order.po_number']; @@ -153,7 +153,7 @@ class VendorHtmlEngine $data['$viewButton'] = &$data['$view_link']; $data['$view_button'] = &$data['$view_link']; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; - $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: ' ', 'label' => ctrans('texts.date')]; + $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->vendor->locale()) ?: ' ', 'label' => ctrans('texts.date')]; $data['$purchase_order.number'] = &$data['$number']; $data['$purchase_order.date'] = &$data['$date']; @@ -177,7 +177,7 @@ class VendorHtmlEngine $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->vendor) ?: ' ', '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()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; + $data['$due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->company->date_format(), $this->vendor->locale()) ?: ' ', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; } else { if ($this->entity->status_id == 1) { $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->amount, $this->vendor) ?: ' ', 'label' => ctrans('texts.balance_due')]; diff --git a/config/ninja.php b/config/ninja.php index 52ba2c4980..eb9345acba 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.6.28'), - 'app_tag' => env('APP_TAG','5.6.28'), + 'app_version' => env('APP_VERSION','5.6.29'), + 'app_tag' => env('APP_TAG','5.6.29'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/database/migrations/2023_08_09_224955_add_nicaragua_currency.php b/database/migrations/2023_08_09_224955_add_nicaragua_currency.php new file mode 100644 index 0000000000..49671a8795 --- /dev/null +++ b/database/migrations/2023_08_09_224955_add_nicaragua_currency.php @@ -0,0 +1,46 @@ +id = 118; + $cur->code = 'NIO'; + $cur->name = 'Nicaraguan Córdoba'; + $cur->symbol = 'C$'; + $cur->thousand_separator = ','; + $cur->decimal_separator = '.'; + $cur->precision = 2; + $cur->save(); + } + + Schema::table('vendors', function (Blueprint $table) { + $table->unsignedInteger('language_id')->nullable(); + $table->timestamp('last_login')->nullable(); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; diff --git a/database/seeders/CurrenciesSeeder.php b/database/seeders/CurrenciesSeeder.php index 65778f979b..3005940fa1 100644 --- a/database/seeders/CurrenciesSeeder.php +++ b/database/seeders/CurrenciesSeeder.php @@ -140,6 +140,7 @@ class CurrenciesSeeder extends Seeder ['id' => 115, 'name' => 'Libyan Dinar', 'code' => 'LYD', 'symbol' => 'LD', 'precision' => '3', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 116, 'name' => 'Silver Troy Ounce', 'code' => 'XAG', 'symbol' => 'XAG', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 117, 'name' => 'Gold Troy Ounce', 'code' => 'XAU', 'symbol' => 'XAU', 'precision' => '3', 'thousand_separator' => ',', 'decimal_separator' => '.'], + ['id' => 118, 'name' => 'Nicaraguan Córdoba', 'code' => 'NIO', 'symbol' => 'C$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ]; foreach ($currencies as $currency) { diff --git a/lang/en/texts.php b/lang/en/texts.php index d75fe8f3bb..f9d8f3821a 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5143,6 +5143,19 @@ $LANG = array( 'is_tax_exempt' => 'Tax Exempt', 'drop_files_here' => 'Drop files here', 'upload_files' => 'Upload Files', + 'triangular_tax_info' => 'Intra-community triangular transaction', + 'intracommunity_tax_info' => 'Tax-free intra-community delivery', + 'reverse_tax_info' => 'Please note that this supply is subject to reverse charge', + 'currency_nicaraguan_cordoba' => 'Nicaraguan Córdoba', + 'public' => 'Public', + 'private' => 'Private', + 'image' => 'Image', + 'other' => 'Other', + 'linked_to' => 'Linked To', + 'file_saved_in_path' => 'The file has been saved in :path', + 'unlinked_transactions' => 'Successfully unlinked :count transactions', + 'unlinked_transaction' => 'Successfully unlinked transaction', + 'view_dashboard_permission' => 'Allow user to access the dashboard, data is limited to available permissions', ); return $LANG; diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index 46bdc30e79..a349743843 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -2189,7 +2189,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'credit_total' => 'Total du crédit', 'mark_billable' => 'Marquer comme facturable', 'billed' => 'Facturé', - 'company_variables' => 'Variables de la compagnie', + 'company_variables' => 'Variables de l\'entreprise', 'client_variables' => 'Variables du client', 'invoice_variables' => 'Variables de facture', 'navigation_variables' => 'Variables de navigation', @@ -2396,6 +2396,9 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'currency_cuban_peso' => 'Cuban Peso', 'currency_bz_dollar' => 'Dollar bélizien', + 'currency_libyan_dinar' => 'Dinar libyen', + 'currency_silver_troy_ounce' => 'Once troy d\'argent', + 'currency_gold_troy_ounce' => 'Once troy d\'or', 'review_app_help' => 'Nous espérons que votre utilisation de cette application vous est agréable.
Un commentaire de votre part serait grandement apprécié!', 'writing_a_review' => 'rédiger un commentaire', @@ -3264,8 +3267,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'contact_custom_value4' => 'Valeur personnalisée du contact 4', 'assigned_to_id' => 'Assigné à ID', 'created_by_id' => 'Créé par ID', - 'add_column' => 'Ajouter colonne', - 'edit_columns' => 'Éditer colonne', + 'add_column' => 'Ajouter une colonne', + 'edit_columns' => 'Éditer les colonnes', 'to_learn_about_gogle_fonts' => 'en savoir plus sur Google Fonts', 'refund_date' => 'Date de remboursement', 'multiselect' => 'Sélection multiple', @@ -3320,7 +3323,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'item_tax_rates' => 'Taux de taxe par article', 'configure_rates' => 'Configuration des taux', 'tax_settings_rates' => 'Taux de taxe', - 'accent_color' => 'Couleur de mise en évidence', + 'accent_color' => 'Couleur d\'accent', 'comma_sparated_list' => 'Liste séparée par virgule', 'single_line_text' => 'Ligne de texte simple', 'multi_line_text' => 'Zone de texte multilignes', @@ -3415,7 +3418,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'quote_details' => 'Informations de la soumission', 'credit_details' => 'Informations de crédit', 'product_columns' => 'Colonnes produit', - 'task_columns' => 'Colonnes tâches', + 'task_columns' => 'Colonnes tâche', 'add_field' => 'Ajouter un champ', 'all_events' => 'Ajouter un événement', 'owned' => 'Propriétaire', @@ -4209,7 +4212,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'copyright' => 'Droits d\'auteur', 'user_created_user' => ':user a créé :created_user à :time', 'company_deleted' => 'Entreprise supprimée', - 'company_deleted_body' => 'La compagnie [:company] a été supprimé par :user', + 'company_deleted_body' => 'L\'entreprise [:company] a été supprimé par :user', 'back_to' => 'Retour à :url', 'stripe_connect_migration_title' => 'Connectez votre compte Stripe', 'stripe_connect_migration_desc' => 'Invoice Ninja v5 utilise Stripe Connect pour lier votre compte Stripe à Invoice Ninja. Cela fournit une couche de sécurité supplémentaire pour votre compte. Maintenant que vos données ont migré, vous devez autoriser Stripe à accepter les paiements dans la v5.

Pour ce faire, accédez à Paramètres > Paiements en ligne > Configurer les passerelles. Cliquez sur Stripe Connect, puis sous Paramètres, cliquez sur Configurer la passerelle. Cela vous amènera à Stripe pour autoriser Invoice Ninja et à votre retour, votre compte sera lié !', @@ -4478,7 +4481,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'id' => 'Id', 'convert_to' => 'Convertir en', 'client_currency' => 'Devise du client', - 'company_currency' => 'Devise de la compagnie', + 'company_currency' => 'Devise de l\'entreprise', 'custom_emails_disabled_help' => 'Il est nécessaire de souscrire à un compte payant pour personnaliser les paramètres anti-pourriels', 'upgrade_to_add_company' => 'Augmenter votre plan pour ajouter des entreprises', 'file_saved_in_downloads_folder' => 'Le fichier a été sauvegardé dans le dossier Téléchargements', @@ -4944,7 +4947,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'sync_from' => 'Sync de', 'gateway_payment_text' => 'Factures: :invoices pour :amount pour :client', 'gateway_payment_text_no_invoice' => 'Paiement sans facture d\'un montant de :amount pour le client :client', - 'click_to_variables' => 'Cliquez ici pour voir toutes les variables', + 'click_to_variables' => 'Cliquez ici pour voir toutes les variables.', 'ship_to' => 'Livrer à', 'stripe_direct_debit_details' => 'Veuillez transférer dans le compte bancaire indiqué ci-dessus.', 'branch_name' => 'Nom de succursale', @@ -5127,9 +5130,13 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'activity_10_online' => ':contact a saisi un paiement :payment pour la facture :invoice pour :client', 'activity_10_manual' => ':user a saisi un paiement :payment pour la facture :invoice pour :client', 'default_payment_type' => 'Type de paiement par défaut', + 'number_precision' => 'Précision du nombre', + 'number_precision_help' => 'Contrôle le nombre de décimales supportées dans l\'interface', + 'is_tax_exempt' => 'Exonéré de taxe', + 'drop_files_here' => 'Déposez les fichiers ici', + 'upload_files' => 'Téléverser les fichiers', ); - return $LANG; -?> \ No newline at end of file +?> diff --git a/resources/views/pdf-designs/business.html b/resources/views/pdf-designs/business.html index b5377d5805..090bee0fb7 100644 --- a/resources/views/pdf-designs/business.html +++ b/resources/views/pdf-designs/business.html @@ -190,7 +190,7 @@ padding-top: 0.5rem; padding-bottom: 0.8rem; padding-left: 0.7rem; - page-break-inside:auto; + /*page-break-inside:auto; this may cause weird breaking*/ overflow: visible !important; } diff --git a/routes/api.php b/routes/api.php index 0331edea84..93a84ea8f6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -157,6 +157,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::resource('clients', ClientController::class); // name = (clients. index / create / show / update / destroy / edit Route::put('clients/{client}/upload', [ClientController::class, 'upload'])->name('clients.upload'); Route::post('clients/{client}/purge', [ClientController::class, 'purge'])->name('clients.purge')->middleware('password_protected'); + Route::post('clients/{client}/updateTaxData', [ClientController::class, 'updateTaxData'])->name('clients.update_tax_data')->middleware('throttle:3,1'); Route::post('clients/{client}/{mergeable_client}/merge', [ClientController::class, 'merge'])->name('clients.merge')->middleware('password_protected'); Route::post('clients/bulk', [ClientController::class, 'bulk'])->name('clients.bulk'); @@ -171,11 +172,11 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('companies/purge/{company}', [MigrationController::class, 'purgeCompany'])->middleware('password_protected'); Route::post('companies/purge_save_settings/{company}', [MigrationController::class, 'purgeCompanySaveSettings'])->middleware('password_protected'); - Route::resource('companies', CompanyController::class); // name = (companies. index / create / show / update / destroy / edit Route::put('companies/{company}/upload', [CompanyController::class, 'upload']); Route::post('companies/{company}/default', [CompanyController::class, 'default']); + Route::post('companies/updateOriginTaxData/{company}', [CompanyController::class, 'updateOriginTaxData'])->middleware('throttle:3,1'); Route::get('company_ledger', [CompanyLedgerController::class, 'index'])->name('company_ledger.index'); @@ -382,6 +383,8 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('subscriptions/bulk', [SubscriptionController::class, 'bulk'])->name('subscriptions.bulk'); Route::get('statics', StaticController::class); // Route::post('apple_pay/upload_file','ApplyPayController::class, 'upload'); + + Route::post('api/v1/yodlee/status/{account_number}', [YodleeController::class, 'accountStatus']); }); Route::post('api/v1/sms_reset', [TwilioController::class, 'generate2faResetCode'])->name('sms_reset.generate')->middleware('throttle:10,1'); diff --git a/tests/Feature/ClientTest.php b/tests/Feature/ClientTest.php index 8fcef3d6c1..0244a3e822 100644 --- a/tests/Feature/ClientTest.php +++ b/tests/Feature/ClientTest.php @@ -41,6 +41,8 @@ class ClientTest extends TestCase use DatabaseTransactions; use MockAccountData; + public $faker; + protected function setUp() :void { parent::setUp(); @@ -63,7 +65,6 @@ class ClientTest extends TestCase $this->makeTestData(); } - public function testClientMergeContactDrop() { diff --git a/tests/Feature/CreditTest.php b/tests/Feature/CreditTest.php index 0e830189e8..f67aa96db9 100644 --- a/tests/Feature/CreditTest.php +++ b/tests/Feature/CreditTest.php @@ -27,6 +27,8 @@ class CreditTest extends TestCase use DatabaseTransactions; use MockAccountData; + public $faker; + protected function setUp(): void { parent::setUp(); diff --git a/tests/Feature/PaymentV2Test.php b/tests/Feature/PaymentV2Test.php index 5f1f386ef1..3365b42adf 100644 --- a/tests/Feature/PaymentV2Test.php +++ b/tests/Feature/PaymentV2Test.php @@ -58,6 +58,78 @@ class PaymentV2Test extends TestCase ); } + public function testUsingDraftCreditsForPayments() + { + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 20, + 'balance' => 20, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => [] + ]); + + $item = InvoiceItemFactory::generateCredit(); + $item['cost'] = 20; + $item['quantity'] = 1; + + $credit = Credit::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'status_id' => Credit::STATUS_DRAFT, + 'uses_inclusive_taxes' => false, + 'amount' => 20, + 'balance' => 0, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => [ + $item + ] + ]); + + $data = [ + 'client_id' => $this->client->hashed_id, + 'invoices' => [ + [ + 'invoice_id' => $invoice->hashed_id, + 'amount' => 20, + ], + ], + 'credits' => [ + [ + 'credit_id' => $credit->hashed_id, + 'amount' => 20, + ], + ], + 'date' => '2020/12/12', + + ]; + + $response = null; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/payments?include=invoices', $data); + + $arr = $response->json(); + $response->assertStatus(200); + + $payment_id = $arr['data']['id']; + $this->assertEquals(Credit::STATUS_APPLIED, $credit->fresh()->status_id); + $this->assertEquals(Invoice::STATUS_PAID, $invoice->fresh()->status_id); + + $this->assertEquals(0, $credit->fresh()->balance); + $this->assertEquals(0, $invoice->fresh()->balance); + + } + public function testStorePaymentWithCreditsThenDeletingInvoices() { $client = Client::factory()->create(['company_id' =>$this->company->id, 'user_id' => $this->user->id, 'balance' => 20, 'paid_to_date' => 0]); diff --git a/tests/Feature/VendorApiTest.php b/tests/Feature/VendorApiTest.php index 79a8800f0f..d103afc5c1 100644 --- a/tests/Feature/VendorApiTest.php +++ b/tests/Feature/VendorApiTest.php @@ -11,13 +11,15 @@ namespace Tests\Feature; +use Tests\TestCase; +use App\Utils\Ninja; +use Tests\MockAccountData; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Session; +use App\Events\Vendor\VendorContactLoggedIn; use Illuminate\Validation\ValidationException; -use Tests\MockAccountData; -use Tests\TestCase; +use Illuminate\Foundation\Testing\DatabaseTransactions; /** * @test @@ -29,6 +31,8 @@ class VendorApiTest extends TestCase use DatabaseTransactions; use MockAccountData; + public $faker; + protected function setUp() :void { parent::setUp(); @@ -42,6 +46,73 @@ class VendorApiTest extends TestCase Model::reguard(); } + public function testVendorLoggedInEvents() + { + $v = \App\Models\Vendor::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id + ]); + + $vc = \App\Models\VendorContact::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'vendor_id' => $v->id + ]); + + $this->assertNull($v->last_login); + $this->assertNull($vc->last_login); + + event(new VendorContactLoggedIn($vc, $this->company, Ninja::eventVars())); + + $this->expectsEvents([VendorContactLoggedIn::class]); + + // $vc->fresh(); + // $v->fresh(); + + // $this->assertNotNull($vc->fresh()->last_login); + // $this->assertNotNull($v->fresh()->last_login); + + } + + public function testVendorLocale() + { + $v = \App\Models\Vendor::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id + ]); + + $this->assertNotNull($v->locale()); + } + + public function testVendorLocaleEn() + { + $v = \App\Models\Vendor::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'language_id' => '1' + ]); + + $this->assertEquals('en', $v->locale()); + } + + public function testVendorLocaleEnCompanyFallback() + { + $settings = $this->company->settings; + $settings->language_id = '2'; + + $c = \App\Models\Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); + + $v = \App\Models\Vendor::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $c->id + ]); + + $this->assertEquals('it', $v->locale()); + } + public function testVendorGetFilter() { $response = $this->withHeaders([ @@ -52,10 +123,79 @@ class VendorApiTest extends TestCase $response->assertStatus(200); } + + public function testAddVendorLanguage200() + { + $data = [ + 'name' => $this->faker->firstName(), + 'language_id' => 2, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/vendors', $data)->assertStatus(200); + + $arr = $response->json(); + $this->assertEquals('2', $arr['data']['language_id']); + + $id = $arr['data']['id']; + + $data = [ + 'language_id' => 3, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/vendors/{$id}", $data); + + $response->assertStatus(200); + + $arr = $response->json(); + $this->assertEquals('3', $arr['data']['language_id']); + + } + + public function testAddVendorLanguage422() + { + $data = [ + 'name' => $this->faker->firstName(), + 'language_id' => '4431', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/vendors', $data)->assertStatus(422); + + } + + + public function testAddVendorLanguage() + { + $data = [ + 'name' => $this->faker->firstName(), + 'language_id' => '1', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/vendors', $data); + + $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals('1', $arr['data']['language_id']); + } + + public function testAddVendorToInvoice() { $data = [ 'name' => $this->faker->firstName(), + 'language_id' => '', ]; $response = $this->withHeaders([ diff --git a/tests/Integration/DTO/AccountSummaryTest.php b/tests/Integration/DTO/AccountSummaryTest.php new file mode 100644 index 0000000000..d420469203 --- /dev/null +++ b/tests/Integration/DTO/AccountSummaryTest.php @@ -0,0 +1,144 @@ +//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 Tests\Integration\DTO; + +use Tests\TestCase; + +/** + * @test + */ +class AccountSummaryTest extends TestCase +{ + + private $data = [ + [ + "CONTAINER"=> "bank", + "providerAccountId"=> 330, + "accountName"=> "Business Acct", + "accountStatus"=> "ACTIVE", + "accountNumber"=> "1012", + "aggregationSource"=> "USER", + "isAsset"=> true, + "balance"=> [ + "currency"=> "AUD", + "amount"=> 44.98, + ], + "id"=> 19315, + "includeInNetWorth"=> true, + "providerId"=> "3857", + "providerName"=> "Bank", + "isManual"=> false, + "availableBalance"=> [ + "currency"=> "AUD", + "amount"=> 34.98, + ], + "currentBalance"=> [ + "currency"=> "AUD", + "amount"=> 344.98, + ], + "accountType"=> "CHECKING", + "displayedName"=> "after David", + "createdDate"=> "2023-01-10T08=>29=>07Z", + "classification"=> "", + "lastUpdated"=> "2023-08-01T23=>50=>13Z", + "nickname"=> "Busines Acct", + "bankTransferCode"=> [ + [ + "id"=> "062", + "type"=> "BSB", + ], + ], + "dataset"=> [ + [ + "name"=> "BASIC_AGG_DATA", + "additionalStatus"=> "AVAILABLE_DATA_RETRIEVED", + "updateEligibility"=> "ALLOW_UPDATE", + "lastUpdated"=> "2023-08-01T23=>49=>53Z", + "lastUpdateAttempt"=> "2023-08-01T23=>49=>53Z", + "nextUpdateScheduled"=> "2023-08-03T14=>45=>14Z", + ], + ], + ] + ]; + + private $bad_data = [ + [ + "CONTAINER"=> "bank", + "providerAccountId"=> 10090, + "accountName"=> "Business Trans Acct", + // "accountStatus"=> "ACTIVE", + "accountNumber"=> "4402", + "aggregationSource"=> "USER", + "isAsset"=> true, + "balance"=> [ + "currency"=> "AUD", + "amount"=> 34.98, + ], + "id"=> 19315, + "includeInNetWorth"=> true, + "providerId"=> "37", + "providerName"=> "Bank", + "isManual"=> false, + // "availableBalance"=> [ + // "currency"=> "AUD", + // "amount"=> 7.98, + // ], + "currentBalance"=> [ + "currency"=> "AUD", + "amount"=> 344.98, + ], + "accountType"=> "CHECKING", + "displayedName"=> "after David", + "createdDate"=> "2023-01-10T08=>29=>07Z", + "classification"=> "SMALL_BUSINESS", + "lastUpdated"=> "2023-08-01T23=>50=>13Z", + "nickname"=> "Busines Acct", + "bankTransferCode"=> [ + [ + "id"=> "060", + "type"=> "BSB", + ], + ], + "dataset"=> [ + [ + "name"=> "BASIC_AGG_DATA", + "additionalStatus"=> "AVAILABLE_DATA_RETRIEVED", + "updateEligibility"=> "ALLOW_UPDATE", + "lastUpdated"=> "2023-08-01T23=>49=>53Z", + "lastUpdateAttempt"=> "2023-08-01T23=>49=>53Z", + "nextUpdateScheduled"=> "2023-08-03T14=>45=>14Z", + ], + ], + ] + ]; + + + + protected function setUp() :void + { + parent::setUp(); + } + + public function testWithBadDataTransformations() + { + $dtox = \App\Helpers\Bank\Yodlee\DTO\AccountSummary::from($this->bad_data[0]); + $this->assertEquals(19315, $dtox->id); + $this->assertEquals('', $dtox->account_status); + } + + public function testTransform() + { + $dto = \App\Helpers\Bank\Yodlee\DTO\AccountSummary::from($this->data[0]); + $this->assertEquals($dto->id, 19315); + } + +} \ No newline at end of file