diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 647f28975c15..c1fa0a6e050a 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -154,10 +154,12 @@ class CompanySettings extends BaseSettings public $email_style_custom = ''; //the template itself public $email_subject_invoice = ''; public $email_subject_quote = ''; + public $email_subject_credit = ''; public $email_subject_payment = ''; public $email_subject_payment_partial = ''; public $email_subject_statement = ''; public $email_template_invoice = ''; + public $email_template_credit = ''; public $email_template_quote = ''; public $email_template_payment = ''; public $email_template_payment_partial = ''; @@ -350,10 +352,12 @@ class CompanySettings extends BaseSettings 'email_signature' => 'string', 'email_subject_invoice' => 'string', 'email_subject_quote' => 'string', + 'email_subject_credit' => 'string', 'email_subject_payment' => 'string', 'email_subject_payment_partial' => 'string', 'email_template_invoice' => 'string', 'email_template_quote' => 'string', + 'email_template_credit' => 'string', 'email_template_payment' => 'string', 'email_template_payment_partial' => 'string', 'email_subject_reminder1' => 'string', diff --git a/app/DataMapper/EmailTemplateDefaults.php b/app/DataMapper/EmailTemplateDefaults.php index b8f657b27faf..39b053b4f800 100644 --- a/app/DataMapper/EmailTemplateDefaults.php +++ b/app/DataMapper/EmailTemplateDefaults.php @@ -30,6 +30,9 @@ class EmailTemplateDefaults case 'email_template_quote': return self::emailQuoteTemplate(); break; + case 'email_template_credit': + return self::emailCreditTemplate(); + break; case 'email_template_payment': return self::emailPaymentTemplate(); break; @@ -69,6 +72,9 @@ class EmailTemplateDefaults case 'email_subject_quote': return self::emailQuoteSubject(); break; + case 'email_subject_credit': + return self::emailCreditSubject(); + break; case 'email_subject_payment': return self::emailPaymentSubject(); break; @@ -109,7 +115,11 @@ class EmailTemplateDefaults public static function emailInvoiceSubject() { return ctrans('texts.invoice_subject', ['number'=>'$number', 'account'=>'$company.name']); - //return Parsedown::instance()->line(self::transformText('invoice_subject')); + } + + public static function emailCreditSubject() + { + return ctrans('texts.credit_subject', ['number'=>'$number', 'account'=>'$company.name']); } public static function emailInvoiceTemplate() @@ -122,14 +132,11 @@ class EmailTemplateDefaults $invoice_message = '
'.self::transformText('invoice_message').'
$view_link
'; return $invoice_message; - //return $converter->convertToHtml($invoice_message); } public static function emailQuoteSubject() { return ctrans('texts.quote_subject', ['number'=>'$number', 'account'=>'$company.name']); - - //return Parsedown::instance()->line(self::transformText('quote_subject')); } public static function emailQuoteTemplate() @@ -158,6 +165,17 @@ class EmailTemplateDefaults } + public static function emailCreditTemplate() + { + $converter = new CommonMarkConverter([ + 'html_input' => 'strip', + 'allow_unsafe_links' => false, + ]); + + return $converter->convertToHtml(self::transformText('credit_message')); + + } + public static function emailPaymentPartialTemplate() { $converter = new CommonMarkConverter([ diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 7cb3c536d8e7..d09befc71655 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -17,6 +17,7 @@ use App\Events\Misc\InvitationWasViewed; use App\Events\Quote\QuoteWasViewed; use App\Http\Controllers\Controller; use App\Models\InvoiceInvitation; +use App\Models\RecurringInvoiceInvitation; use App\Utils\Ninja; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 5d8555bd9530..6e0187ed7fce 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -13,6 +13,7 @@ namespace App\Http\Requests\Invoice; use App\Http\Requests\Request; use App\Http\ValidationRules\Invoice\UniqueInvoiceNumberRule; +use App\Http\ValidationRules\Project\ValidProjectForClient; use App\Models\ClientContact; use App\Models\Invoice; use App\Utils\Traits\CleanLineItems; @@ -47,12 +48,14 @@ class StoreInvoiceRequest extends Request $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; } - $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; + $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id; $rules['invitations.*.client_contact_id'] = 'distinct'; $rules['number'] = new UniqueInvoiceNumberRule($this->all()); + $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; + return $rules; } @@ -68,6 +71,10 @@ class StoreInvoiceRequest extends Request $input['client_id'] = $this->decodePrimaryKey($input['client_id']); } + if (array_key_exists('project_id', $input) && is_string($input['project_id'])) { + $input['project_id'] = $this->decodePrimaryKey($input['project_id']); + } + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); } diff --git a/app/Http/Requests/Project/StoreProjectRequest.php b/app/Http/Requests/Project/StoreProjectRequest.php index f17afb073761..6c1d3ef94fec 100644 --- a/app/Http/Requests/Project/StoreProjectRequest.php +++ b/app/Http/Requests/Project/StoreProjectRequest.php @@ -33,7 +33,8 @@ class StoreProjectRequest extends Request { $rules = []; - $rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId(); + //$rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId(); + $rules['name'] = 'required'; $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; return $rules; diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 67fb0ef95051..0e21a96e7802 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -12,6 +12,7 @@ namespace App\Http\Requests\RecurringInvoice; use App\Http\Requests\Request; +use App\Http\ValidationRules\Recurring\UniqueRecurringInvoiceNumberRule; use App\Models\Client; use App\Models\RecurringInvoice; use App\Utils\Traits\CleanLineItems; @@ -52,6 +53,8 @@ class StoreRecurringInvoiceRequest extends Request $rules['frequency_id'] = 'required|integer'; + $rules['number'] = new UniqueRecurringInvoiceNumberRule($this->all()); + return $rules; } diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index 7a6cbfbe656f..f5782f7ad989 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -48,6 +48,10 @@ class UpdateRecurringInvoiceRequest extends Request $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; } + if ($this->input('number')) { + $rules['number'] = 'unique:recurring_invoices,number,'.$this->id.',id,company_id,'.$this->recurring_invoice->company_id; + } + return $rules; } diff --git a/app/Http/Requests/Vendor/StoreVendorRequest.php b/app/Http/Requests/Vendor/StoreVendorRequest.php index df27f39ea8b6..f892593a51b6 100644 --- a/app/Http/Requests/Vendor/StoreVendorRequest.php +++ b/app/Http/Requests/Vendor/StoreVendorRequest.php @@ -48,10 +48,13 @@ class StoreVendorRequest extends Request protected function prepareForValidation() { - // $input = $this->all(); + $input = $this->all(); - - // $this->replace($input); + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { + $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); + } + + $this->replace($input); } public function messages() diff --git a/app/Http/Requests/Vendor/UpdateVendorRequest.php b/app/Http/Requests/Vendor/UpdateVendorRequest.php index bd8b44d6966b..8a6cba26d39f 100644 --- a/app/Http/Requests/Vendor/UpdateVendorRequest.php +++ b/app/Http/Requests/Vendor/UpdateVendorRequest.php @@ -69,6 +69,10 @@ class UpdateVendorRequest extends Request { $input = $this->all(); + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { + $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); + } + $this->replace($input); } } diff --git a/app/Http/ValidationRules/Project/ValidProjectForClient.php b/app/Http/ValidationRules/Project/ValidProjectForClient.php new file mode 100644 index 000000000000..a23e421de398 --- /dev/null +++ b/app/Http/ValidationRules/Project/ValidProjectForClient.php @@ -0,0 +1,55 @@ +input = $input; + } + /** + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + if(is_string($this->input['project_id'])) + $this->input['project_id'] = $this->decodePrimaryKey($this->input['project_id']); + + $project = Project::findOrFail($this->input['project_id']); + + return $project->client_id == $this->input['client_id']; + } + + /** + * @return string + */ + public function message() + { + return "Project client does not match entity client"; + } + + +} diff --git a/app/Http/ValidationRules/Recurring/UniqueRecurringInvoiceNumberRule.php b/app/Http/ValidationRules/Recurring/UniqueRecurringInvoiceNumberRule.php new file mode 100644 index 000000000000..2103424b6e14 --- /dev/null +++ b/app/Http/ValidationRules/Recurring/UniqueRecurringInvoiceNumberRule.php @@ -0,0 +1,73 @@ +input = $input; + } + + /** + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return $this->checkIfInvoiceNumberUnique(); //if it exists, return false! + } + + /** + * @return string + */ + public function message() + { + return "Recurring Invoice number {$this->input['number']} already taken"; + } + + /** + * @param $email + * + * //off,when_sent,when_paid + * + * @return bool + */ + private function checkIfInvoiceNumberUnique() : bool + { + if(empty($this->input['number'])) + return true; + + $invoice = RecurringInvoice::where('client_id', $this->input['client_id']) + ->where('number', $this->input['number']) + ->withTrashed() + ->exists(); + + if ($invoice) { + return false; + } + + return true; + } +} diff --git a/app/Models/RecurringInvoiceInvitation.php b/app/Models/RecurringInvoiceInvitation.php index 31aeb6043363..d958f6b28a1b 100644 --- a/app/Models/RecurringInvoiceInvitation.php +++ b/app/Models/RecurringInvoiceInvitation.php @@ -50,7 +50,7 @@ class RecurringInvoiceInvitation extends BaseModel */ public function contact() { - return $this->belongsTo(ClientContact::class)->withTrashed(); + return $this->belongsTo(ClientContact::class, 'client_contact_id', 'id')->withTrashed(); } /** diff --git a/app/Repositories/VendorRepository.php b/app/Repositories/VendorRepository.php index 6f241f8187a5..fca84a6ba067 100644 --- a/app/Repositories/VendorRepository.php +++ b/app/Repositories/VendorRepository.php @@ -57,6 +57,7 @@ class VendorRepository extends BaseRepository */ public function save(array $data, Vendor $vendor) : ?Vendor { + $vendor->fill($data); $vendor->save(); diff --git a/app/Transformers/CompanyLedgerTransformer.php b/app/Transformers/CompanyLedgerTransformer.php index d5214b7fca35..ffc082e3a1a5 100644 --- a/app/Transformers/CompanyLedgerTransformer.php +++ b/app/Transformers/CompanyLedgerTransformer.php @@ -28,7 +28,7 @@ class CompanyLedgerTransformer extends EntityTransformer */ public function transform(CompanyLedger $company_ledger) { - $entity_name = lcfirst(class_basename($company_ledger->company_ledgerable_type)).'_id'; + $entity_name = lcfirst(rtrim(class_basename($company_ledger->company_ledgerable_type), 's')).'_id'; return [ $entity_name => (string) $this->encodePrimaryKey($company_ledger->company_ledgerable_id), diff --git a/app/Utils/Traits/AppSetup.php b/app/Utils/Traits/AppSetup.php index 9c303823fa2b..c28e8ea66f90 100644 --- a/app/Utils/Traits/AppSetup.php +++ b/app/Utils/Traits/AppSetup.php @@ -104,6 +104,10 @@ trait AppSetup 'subject' => EmailTemplateDefaults::emailStatementSubject(), 'body' => EmailTemplateDefaults::emailStatementTemplate(), ], + 'credit' => [ + 'subject' => EmailTemplateDefaults::emailCreditSubject(), + 'body' => EmailTemplateDefaults::emailCreditTemplate(), + ], ]; Cache::forever($name, $data); diff --git a/app/Utils/Traits/Inviteable.php b/app/Utils/Traits/Inviteable.php index 0b7012738ac5..3ff2afda17af 100644 --- a/app/Utils/Traits/Inviteable.php +++ b/app/Utils/Traits/Inviteable.php @@ -11,6 +11,8 @@ namespace App\Utils\Traits; +use Illuminate\Support\Str; + /** * Class Inviteable. */ @@ -42,7 +44,9 @@ trait Inviteable public function getLink() :string { - $entity_type = strtolower(class_basename($this->entityType())); + //$entity_type = strtolower(class_basename($this->entityType())); + + $entity_type = Str::snake(class_basename($this->entityType())); //$this->with('company','contact',$this->entity_type); //$this->with('company'); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 57881f1c32b5..c5ee6460babd 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -3283,5 +3283,7 @@ return [ 'saved_at' => 'Saved at :time', 'credit_payment' => 'Credit applied to Invoice :invoice_number', + 'credit_subject' => 'New credit :number from :account', + 'credit_message' => 'To view your credit for :amount, click the link below.', ]; diff --git a/routes/client.php b/routes/client.php index 685482d0ed5b..b08dd2739b65 100644 --- a/routes/client.php +++ b/routes/client.php @@ -31,7 +31,7 @@ Route::group(['middleware' => ['auth:contact', 'locale'], 'prefix' => 'client', Route::get('invoices/{invoice_invitation}', 'ClientPortal\InvoiceController@show')->name('invoice.show_invitation'); Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index')->middleware('portal_enabled'); - Route::get('recurring_invoices/{recurring_invoice}', 'ClientPortal\RecurringInvoiceController@show')->name('recurring_invoices.show'); + Route::get('recurring_invoices/{recurring_invoice}', 'ClientPortal\RecurringInvoiceController@show')->name('recurring_invoice.show'); Route::get('recurring_invoices/{recurring_invoice}/request_cancellation', 'ClientPortal\RecurringInvoiceController@requestCancellation')->name('recurring_invoices.request_cancellation'); Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process'); diff --git a/tests/Feature/Shop/ShopInvoiceTest.php b/tests/Feature/Shop/ShopInvoiceTest.php index 75a388bdd188..0ca066f580d5 100644 --- a/tests/Feature/Shop/ShopInvoiceTest.php +++ b/tests/Feature/Shop/ShopInvoiceTest.php @@ -99,6 +99,8 @@ class ShopInvoiceTest extends TestCase $this->company->enable_shop_api = true; $this->company->save(); + Product::truncate(); + $product = Product::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id,