├── README.md └── app ├── controllers ├── book_controller.rb └── vehicle_listings_controller.rb ├── decorators ├── automatic_delegation.rb ├── lead_decorator.rb └── offer_decorator.rb ├── facades ├── dashboard_facade.rb └── documents_facade.rb ├── forms └── investor_form.rb ├── helpers ├── lead_helper.rb └── partners_helper.rb ├── models ├── investor.rb ├── investor_fat.rb ├── loan.rb ├── report.rb └── vehicle_listing.rb ├── performers ├── investment_data_performer.rb ├── investment_performer.rb ├── investor_performer.rb └── loan_data_performer.rb ├── policies ├── cart_policy.rb ├── post_policy.rb └── store_front_policy.rb ├── presenters ├── dividend_year_dispatcher.rb ├── monthly_distribution_presenter.rb ├── quarterly_distribution_presenter.rb └── yearly_distribution_presenter.rb ├── serializers ├── chat_attachment_serializer.rb ├── chat_message_serializer.rb └── review_serializer.rb ├── services ├── calculate_currency.rb └── connect_listings_to_seller.rb └── workers ├── check_ad_worker.rb ├── move_to_top_worker.rb └── partner_deactivate_worker.rb /README.md: -------------------------------------------------------------------------------- 1 | # Rails Structure Sample 2 | 3 | ![Rails Structure](https://www.iconsdb.com/icons/preview/icon-sets/web-2-ruby-red/tree-80-xxl.png) 4 | 5 | ## Description 6 | 7 | Ruby on Rails is one of our favourite frameworks for web applications development. We love it because of agile and interesting development process, high performance and, of course, ruby programming language. 8 | Below you can find an example of a project`s structure, and its main components 9 | 10 | ## File structure 11 | 12 | ``` 13 | project_name 14 | ├── app 15 | | ├── assets => images, fonts, stylesheets, js 16 | | ├── controllers 17 | | ├── decorators 18 | | ├── helpers 19 | | ├── mailers 20 | | ├── models 21 | | ├── performers 22 | | ├── policies 23 | | ├── presenters 24 | | ├── serializers 25 | | ├── services 26 | | ├── uploaders 27 | | ├── views 28 | | └── workers => workers for running processes in the background 29 | ├── bin => contains script that starts, update, deploy or run your application. 30 | ├── config => configure your application's routes, database, and more 31 | ├── db => contains your current database schema and migrations 32 | ├── lib => extended modules for your application 33 | ├── log => app log files 34 | ├── public => The only folder seen by the world as-is. Contains static files and compiled assets. 35 | ├── spec => Unit tests, features, and other test apparatus. 36 | ├── tmp => Temporary files (like cache and pid files). 37 | └── vendor => third-party code 38 | ``` 39 | 40 | ## Controllers 41 | 42 | Action Controller is the C in MVC. After routing has determined which controller to use for a request, your controller is responsible for making sense of the request and producing the appropriate output. 43 | 44 | There are a lot of opinions on what a controller should do. A common ground of the responsibilities a controller should have include the following: 45 | 46 | **Authentication and authorization** — checking whether the entity (oftentimes, a user) behind the request is who it says it is and whether it is allowed to access the resource or perform the action. 47 | 48 | **Data fetching** — it should call the logic for finding the right data based on the parameters that came with the request. In the perfect world, it should be a call to one method that does all the work. The controller should not do the heavy work, it should delegate it further. 49 | 50 | **Template rendering** — finally, it should return the right response by rendering the result with the proper format (HTML, JSON, etc.). Or, it should redirect to some other path or URL. 51 | 52 | 53 | ```ruby 54 | class BooksController < ApplicationController 55 | 56 | def show 57 | @book = Book.find(params[:id]) 58 | end 59 | 60 | def create 61 | @book = Book.new(book_params) 62 | 63 | if @book.save 64 | redirect_to action: 'list' 65 | else 66 | @subjects = Subject.all 67 | render action: 'new' 68 | end 69 | end 70 | 71 | private 72 | 73 | def book_params 74 | params.require(:books).permit(:title, :price, :subject_id, :description) 75 | end 76 | 77 | end 78 | ``` 79 | 80 | ```ruby 81 | class ListingsController < ApiController 82 | 83 | def index 84 | listings = Current.user.listings 85 | 86 | render json: ListingSerializer.new(listings).serializable_hash 87 | end 88 | 89 | def create 90 | authorize Listing 91 | 92 | command = Listings::Create.call(Current.user, params[:listing]) 93 | 94 | if command.success? 95 | render json: ListingSerializer.new(command.result).serializable_hash 96 | else 97 | render json: command.errors 98 | end 99 | end 100 | 101 | def destroy 102 | listing = Current.user.listings.find(params[:id]) 103 | 104 | authorize listing 105 | 106 | command = Listings::Delete.call(listing) 107 | 108 | if command.success? 109 | head :ok 110 | else 111 | render json: command.errors 112 | end 113 | end 114 | 115 | end 116 | ``` 117 | 118 | [Examples](app/controllers) 119 | 120 | [Documentation](https://edgeguides.rubyonrails.org/action_controller_overview.html) 121 | 122 | ## Decorators 123 | 124 | Decorators adds an object-oriented layer of presentation logic to your Rails application 125 | 126 | ```ruby 127 | class ListingDecorator < Draper::Decorator 128 | 129 | delegate_all 130 | 131 | def creation_date 132 | object.created_at.strftime('%d/%m/%Y') 133 | end 134 | 135 | end 136 | ``` 137 | 138 | [Examples](app/decorators) 139 | 140 | [Documentation](https://github.com/drapergem/draper) 141 | 142 | ## Helpers 143 | 144 | Helpers in Rails are used to extract complex logic out of the view so that you can organize your code better. 145 | 146 | ```ruby 147 | module PartnersHelper 148 | 149 | def partner_activation(partner) 150 | partner.active? ? t('partners.extend_activation') : t('partners.activation') 151 | end 152 | 153 | def partner_edit_page_title(redirect_url) 154 | redirect_url.present? ? I18n.t('partners.upgrade_my_profile') : I18n.t('partners.edit_my_profile') 155 | end 156 | 157 | end 158 | ``` 159 | 160 | [Examples](app/helpers) 161 | 162 | [Documentation](https://api.rubyonrails.org/classes/ActionController/Helpers.html) 163 | 164 | ## Models 165 | 166 | Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. 167 | 168 | ```ruby 169 | # == Schema Information 170 | # 171 | # Table name: colors 172 | # 173 | # id :integer not null, primary key 174 | # created_at :datetime not null 175 | # updated_at :datetime not null 176 | # 177 | 178 | class Color < ApplicationRecord 179 | 180 | # == Constants ============================================================ 181 | 182 | # == Attributes =========================================================== 183 | attribute :name 184 | 185 | # == Extensions =========================================================== 186 | translates :name, fallbacks_for_empty_translations: true 187 | globalize_accessors 188 | 189 | # == Relationships ======================================================== 190 | has_many :vehicle_listings, dependent: :nullify 191 | 192 | # == Validations ========================================================== 193 | validates(*Color.globalize_attribute_names, presence: true) 194 | 195 | # == Scopes =============================================================== 196 | 197 | # == Callbacks ============================================================ 198 | 199 | # == Class Methods ======================================================== 200 | 201 | # == Instance Methods ===================================================== 202 | 203 | end 204 | ``` 205 | 206 | [Examples](app/models) 207 | 208 | [Documentation](https://guides.rubyonrails.org/active_model_basics.html) 209 | 210 | [Ruby on Rails Model Patterns and Anti-patterns](https://blog.appsignal.com/2020/11/18/rails-model-patterns-and-anti-patterns.html) 211 | 212 | ## Form Objects 213 | 214 | Use [form object](https://medium.com/selleo/essential-rubyonrails-patterns-form-objects-b199aada6ec9) pattern to make your models more lightweight. 215 | Single responsibility principle helps us make the best design decisions about what the class should be responsible for. Your model is a database table, it represents a single entry in the code, so there is no reason to worry and user action. 216 | 217 | Form Objects are responsible for the presentation of the form in your application. Each form field is an attribute in a class and can be checked through validation, which will give us clean data and they will go further along the chain. This can be your model, defining a table in the database or, for example, a search form. 218 | 219 | Here are an examples of [fat](app/models/investor_fat.rb) and [refactored](app/models/investor.rb) models. 220 | 221 | Usage Form Object in controller: 222 | 223 | ```ruby 224 | 225 | class Admin::InvestorsController < CommonAdminController 226 | 227 | include Sorting 228 | 229 | load_resource :investor, except: %i[index new create] 230 | 231 | def index 232 | @investors = Investor.real.includes(:investor_group, :address) 233 | end 234 | 235 | def new 236 | @investor_form = InvestorForm.new 237 | end 238 | 239 | def edit 240 | @investor_form = InvestorForm.new(@investor) 241 | end 242 | 243 | def create 244 | params[:investor][:password] = generate_devise_password 245 | @investor_form = InvestorForm.new 246 | 247 | if @investor_form.submit(params) 248 | flash[:success] = 'Investor was successfully created.' 249 | redirect_to admin_investor_path(@investor_form.id) 250 | else 251 | render :new 252 | end 253 | end 254 | 255 | def update 256 | @investor_form = InvestorForm.new(@investor) 257 | 258 | if @investor_form.submit(params) 259 | flash[:success] = 'Investor was successfully updated.' 260 | redirect_to admin_investor_path(@investor) 261 | else 262 | render :edit 263 | end 264 | end 265 | 266 | ``` 267 | 268 | [Examples](app/forms/investor_form.rb) 269 | 270 | [Documentation](https://thoughtbot.com/blog/activemodel-form-objects) 271 | 272 | ## Performers 273 | 274 | The performer pattern creates seperation for the methods in your model that are view related. The performers are modules and are included into the corresponding model. 275 | 276 | ```ruby 277 | module InvestmentDataPerformer 278 | 279 | def self.included(base) 280 | base.extend ClassMethods 281 | end 282 | 283 | module ClassMethods 284 | 285 | end 286 | 287 | def non_final_profit 288 | return nil unless investment.non_final_profit 289 | investment.non_final_profit * holding / 100 290 | end 291 | 292 | def final_distribution 293 | return unless investment.distribution.present? 294 | investment.distribution * holding / 100 295 | end 296 | 297 | end 298 | ``` 299 | 300 | [Examples](app/performers) 301 | 302 | [Documentation](https://github.com/jwipeout/performer-pattern) 303 | 304 | ## Serializers 305 | 306 | In Ruby serialization does nothing else than just converting the given data structure into its valid JSON. 307 | 308 | ```ruby 309 | class AttachmentSerializer 310 | 311 | include JSONAPI::Serializer 312 | 313 | attributes :id, :name, :description 314 | 315 | attribute :url, &:attachment_url 316 | 317 | attribute :type do |obj| 318 | obj.attachment.mime_type 319 | end 320 | 321 | attribute :name do |obj| 322 | obj.attachment['filename'] 323 | end 324 | 325 | end 326 | ``` 327 | 328 | [Examples](app/serializers) 329 | 330 | [Documentation](https://github.com/jsonapi-serializer/jsonapi-serializer) 331 | 332 | ## Services 333 | 334 | Service Object can be a class or module in Ruby that performs an action. It can help take out logic from other areas of the MVC files. 335 | 336 | ```ruby 337 | class Subscribe < BaseService 338 | 339 | attr_reader :email 340 | 341 | def initialize(email) 342 | @email = email 343 | end 344 | 345 | def call 346 | list.members.create(body: { email_address: email, status: 'subscribed' }) 347 | end 348 | 349 | private 350 | 351 | def list 352 | gibbon_request.lists(ENV['MAILCHIMP_LIST_ID']) 353 | end 354 | 355 | def gibbon_request 356 | Gibbon::Request.new(api_key: ENV['MAILCHIMP_API_KEY']) 357 | end 358 | 359 | end 360 | ``` 361 | 362 | [Examples](app/services) 363 | 364 | [Documentation](https://github.com/nebulab/simple_command) 365 | 366 | ## Presenters 367 | 368 | Presenters give you an object oriented way to approach view helpers. 369 | 370 | ```ruby 371 | class QuarterlyDistributionPresenter < BaseDividendYearPresenter 372 | 373 | QUARTERS = %w[Q1 Q2 Q3 Q4].freeze 374 | 375 | def dividends_year 376 | QUARTERS.map do |quarter| 377 | value_decorator(quarter) 378 | end 379 | end 380 | 381 | def select_dividend_by(dividend_year) 382 | CalendarQuarter.from_date(dividend_year).quarter 383 | end 384 | 385 | end 386 | ``` 387 | 388 | [Examples](app/presenters) 389 | 390 | [Documentation](http://nithinbekal.com/posts/rails-presenters/) 391 | 392 | ## Policies 393 | 394 | Pundit provides a set of helpers which guide in leveraging regular Ruby classes and object oriented design patterns to build a simple, robust and scalable authorization system. 395 | 396 | ```ruby 397 | class ProductPolicy < ApplicationPolicy 398 | 399 | def create? 400 | user_active? 401 | end 402 | 403 | def show? 404 | user_active? && record.active? 405 | end 406 | 407 | def destroy? 408 | show? 409 | end 410 | 411 | end 412 | 413 | ``` 414 | 415 | [Examples](app/policies) 416 | 417 | [Documentation](https://github.com/varvet/pundit) 418 | 419 | ## Facades 420 | 421 | Facades provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use. 422 | One of the responsibilities that is being thrown at the controller is to prepare the data so it can be presented to user. To remove the responsibility of preparing the data for the view we're using Facade pattern. 423 | 424 | ```ruby 425 | class Admin 426 | 427 | class DocumentsFacade 428 | 429 | attr_reader :loan, :investor 430 | 431 | def initialize(loan_id, investor_id) 432 | @loan = Loan.find(loan_id) 433 | @investor = Investor.find(investor_id) 434 | end 435 | 436 | def lender? 437 | investor.present? 438 | end 439 | 440 | def documents 441 | Document.where(investment_id: loan.id, investor_id: investor.id) 442 | end 443 | 444 | def lenders 445 | loan.lenders.order(:name).uniq.collect { |l| [l.name, l.id] } 446 | end 447 | 448 | end 449 | 450 | end 451 | ``` 452 | 453 | Now we can reduce our controller down to: 454 | 455 | ```ruby 456 | class DocumentsController < LoansController 457 | 458 | def index 459 | add_breadcrumb 'Documents' 460 | @documents_data = Admin::DocumentsFacade.new(params[:loan_id], params[:investor_id]) 461 | end 462 | 463 | end 464 | ``` 465 | 466 | And inside our view we will call for our facade: 467 | 468 | ```slim 469 | = render ‘documents/form’, document: @document_data.new_document 470 | ``` 471 | 472 | [Examples](app/facades) 473 | 474 | [Documentation](https://medium.com/kkempin/facade-design-pattern-in-ruby-on-rails-710aa88326f) 475 | 476 | ## Workers 477 | 478 | At Codica we use sidekiq as a full-featured background processing framework for Ruby. It aims to be simple to integrate with any modern Rails application and much higher performance than other existing solutions. 479 | 480 | ```ruby 481 | class PartnerAlertWorker 482 | 483 | include Sidekiq::Worker 484 | 485 | def perform(id) 486 | @partner = Partner.find(id) 487 | PartnerMailer.alert(@partner).deliver_now 488 | end 489 | 490 | end 491 | ``` 492 | 493 | [Examples](app/workers) 494 | 495 | [Documentation](https://github.com/mperham/sidekiq/wiki) 496 | 497 | ## Spec 498 | 499 | Spec folder include the test suites, the application logic tests. 500 | 501 | ```ruby 502 | require 'rails_helper' 503 | 504 | RSpec.feature 'Static page', type: :feature do 505 | let(:listing) { create :listing } 506 | 507 | scenario 'unsuccess payment' do 508 | visit payment_info_error_path(listing: listing.id) 509 | expect(page).to have_content I18n.t('unsuccess_payment_header') 510 | end 511 | 512 | scenario 'success payment' do 513 | visit payment_info_success_path(promotion: 'on_homepage', price: '30') 514 | expect(page).to have_content I18n.t('success_payment_header') 515 | end 516 | 517 | end 518 | ``` 519 | 520 | [Examples](https://github.com/codica2/rspec-samples) 521 | 522 | [Documentation](https://relishapp.com/rspec/) 523 | 524 | ## License 525 | rails-app-best-practice is Copyright © 2015-2019 Codica. It is released under the [MIT License](https://opensource.org/licenses/MIT). 526 | 527 | ## About Codica 528 | 529 | [![Codica logo](https://www.codica.com/assets/images/logo/logo.svg)](https://www.codica.com) 530 | 531 | We love open source software! See [our other projects](https://github.com/codica2) or [hire us](https://www.codica.com/) to design, develop, and grow your product. 532 | -------------------------------------------------------------------------------- /app/controllers/book_controller.rb: -------------------------------------------------------------------------------- 1 | class BooksController < ApplicationController 2 | 3 | def show 4 | @book = Book.find(params[:id]) 5 | end 6 | 7 | def new 8 | @book = Book.new 9 | @subjects = Subject.all 10 | end 11 | 12 | def create 13 | @book = Book.new(book_params) 14 | 15 | if @book.save 16 | redirect_to action: 'list' 17 | else 18 | @subjects = Subject.all 19 | render action: 'new' 20 | end 21 | end 22 | 23 | def edit 24 | @book = Book.find(params[:id]) 25 | @subjects = Subject.all 26 | end 27 | 28 | def update 29 | @book = Book.find(params[:id]) 30 | 31 | if @book.update_attributes(book_params) 32 | redirect_to action: 'show', id: @book 33 | else 34 | @subjects = Subject.all 35 | render action: 'edit' 36 | end 37 | end 38 | 39 | private 40 | 41 | def book_params 42 | params.require(:books).permit(:title, :price, :subject_id, :description) 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /app/controllers/vehicle_listings_controller.rb: -------------------------------------------------------------------------------- 1 | module Dashboards 2 | 3 | class VehicleListingsController < BaseController 4 | 5 | layout 'dashboard' 6 | 7 | def index 8 | authorize! :manage, VehicleListing 9 | 10 | add_breadcrumb I18n.t('shared.footer.home'), :root_path 11 | add_breadcrumb I18n.t('ads.ad.my_profile'), dashboard_path(locale: I18n.locale) 12 | 13 | @price_range = Calculate::PriceRange.call(VehicleListing.prices, @current_currency) 14 | @page = Page::Listing::Vehicle::Search.new(listings: current_seller.vehicle_listings, params: params) 15 | 16 | current_seller.visit!(:my_vehicles) 17 | 18 | respond_to do |format| 19 | format.html 20 | format.js 21 | end 22 | end 23 | 24 | private 25 | 26 | def listings_params 27 | return {} if params[:listing].blank? 28 | 29 | params.require(:listing).permit(:city_id, :body_id, :model_id, :car_type, :brand_id, :start_date) 30 | end 31 | 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /app/decorators/automatic_delegation.rb: -------------------------------------------------------------------------------- 1 | module AutomaticDelegation 2 | 3 | extend ActiveSupport::Concern 4 | 5 | # Delegates missing instance methods to the source object. Note: This will delegate `super` 6 | # method calls to `object` as well. Calling `super` will first try to call the method on 7 | # the parent decorator class. If no method exists on the parent class, it will then try 8 | # to call the method on the `object`. 9 | def method_missing(method, *args, &block) 10 | return super unless delegatable?(method) 11 | 12 | object.send(method, *args, &block) 13 | end 14 | 15 | # Checks if the decorator responds to an instance method, or is able to 16 | # proxy it to the source object. 17 | def respond_to_missing?(method, include_private = false) 18 | super || delegatable?(method) 19 | end 20 | 21 | # @private 22 | def delegatable?(method) 23 | return if private_methods(false).include?(method) 24 | 25 | object.respond_to?(method) 26 | end 27 | 28 | module ClassMethods 29 | 30 | # Proxies missing class methods to the source class. 31 | def method_missing(method, *args, &block) 32 | return super unless delegatable?(method) 33 | 34 | object_class.send(method, *args, &block) 35 | end 36 | 37 | # Checks if the decorator responds to a class method, or is able to proxy 38 | # it to the source class. 39 | def respond_to_missing?(method, include_private = false) 40 | super || delegatable?(method) 41 | end 42 | 43 | # @private 44 | def delegatable?(method) 45 | object_class? && object_class.respond_to?(method) 46 | end 47 | 48 | # @private 49 | # Avoids reloading the model class when ActiveSupport clears autoloaded 50 | # dependencies in development mode. 51 | def before_remove_const; end 52 | 53 | end 54 | 55 | included do 56 | private :delegatable? 57 | private_class_method :delegatable? 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /app/decorators/lead_decorator.rb: -------------------------------------------------------------------------------- 1 | class LeadDecorator < Draper::Decorator 2 | 3 | include ActionView::Helpers::NumberHelper 4 | 5 | delegate_all 6 | 7 | def offered_price 8 | number_to_currency(object.offered_price, unit: ExchangeRate.base_currency, precision: 0, format: price_format, delimiter: price_divider) 9 | end 10 | 11 | private 12 | 13 | def price_format 14 | I18n.default_locale == :fr ? '%n %u' : '%u %n' 15 | end 16 | 17 | def price_divider 18 | I18n.default_locale == :en ? ',' : ' ' 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /app/decorators/offer_decorator.rb: -------------------------------------------------------------------------------- 1 | class OfferDecorator < Draper::Decorator 2 | 3 | include ActionView::Helpers::NumberHelper 4 | 5 | delegate_all 6 | 7 | def creation_date 8 | object.created_at.strftime('%d/%m/%Y') 9 | end 10 | 11 | def title 12 | object.target.decorate.title 13 | end 14 | 15 | def recipients_count 16 | recipients = object.leads.count 17 | result = object.with_sms ? "#{recipients} SMS" : nil 18 | [result, "#{recipients} EMAILS"].reject(&:blank?).join(' / ') 19 | end 20 | 21 | def price 22 | number_to_currency(object.price, unit: '$') 23 | end 24 | 25 | def emails_delivery_status 26 | "#{object.emails_delivered} / #{object.lead_offers.count}" 27 | end 28 | 29 | def sms_delivery_status 30 | return unless object.with_sms? 31 | 32 | "#{object.sms_delivered} / #{object.lead_offers.count}" 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /app/facades/dashboard_facade.rb: -------------------------------------------------------------------------------- 1 | class Admin 2 | class DashboardFacade 3 | 4 | def initialize; end 5 | 6 | def total_invest_amount(type) 7 | InvestmentData.send(type).joins(:amount_sources).sum(:'amount_sources.amount') 8 | end 9 | 10 | def total_loan_amount 11 | LoanData.joins(:amount_sources).sum(:'amount_sources.amount') 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/facades/documents_facade.rb: -------------------------------------------------------------------------------- 1 | class Admin 2 | class DocumentsFacade 3 | 4 | attr_reader :loan, :investor 5 | 6 | def initialize(loan_id, investor_id) 7 | @loan = Loan.find(loan_id) 8 | @investor = Investor.find(investor_id) if investor_id.present? 9 | end 10 | 11 | def lender? 12 | @investor.present? 13 | end 14 | 15 | def new_document 16 | @loan.documents.build(investor_id: @investor.id) 17 | end 18 | 19 | def documents 20 | Document.where(investment_id: @loan.id, investor_id: @investor.id) 21 | end 22 | 23 | def lenders 24 | @loan.lenders.order(:name).uniq.collect { |l| [l.name, l.id] } 25 | end 26 | 27 | def placeholder 28 | lender? ? 'Please select lender' : 'Lenders' 29 | end 30 | 31 | def selected_lender 32 | @investor ? @investor.id : nil 33 | end 34 | 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /app/forms/investor_form.rb: -------------------------------------------------------------------------------- 1 | class InvestorForm 2 | include ActiveModel::Model 3 | 4 | validates :name, presence: { message: "Can't be blank" } 5 | validate :email_uniqueness, if: :email_validation 6 | 7 | validates :email, 8 | presence: { message: "Can't be blank" }, 9 | format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i, message: 'Not a valid email address' } 10 | 11 | validates :phone_number, 12 | presence: { message: "Can't be blank" }, 13 | format: { with: /\A\+?[\d]+[\d\-]+\z/, message: 'Invalid phone number' }, 14 | length: { minimum: 10, maximum: 15, message: 'The amount of characters is between 10 and 15' } 15 | 16 | # attr_accessor :check, :wire_transfer 17 | attr_reader :params, :address 18 | 19 | delegate :check, :wire_transfer, :connected?, :persisted?, :inactive?, 20 | :investor_viewers, :viewer_investors, :investor_group, :address, :inactive_investor_contact, to: :investor 21 | 22 | delegate :address_attributes=, to: :investor, prefix: false 23 | delegate :inactive_investor_contact_attributes=, to: :investor, prefix: false 24 | 25 | def self.model_name 26 | ActiveModel::Name.new(self, nil, 'Investor') 27 | end 28 | 29 | def initialize(instance = nil) 30 | @investor = instance 31 | investor.build_address unless investor.address 32 | investor.build_inactive_investor_contact if investor && investor.inactive_investor_contact.nil? 33 | 34 | investor_params_array.each do |key| 35 | singleton_class.send :attr_accessor, key 36 | singleton_class.send :delegate, key, to: :investor 37 | end 38 | end 39 | 40 | def submit(params) 41 | @params = params 42 | investor.assign_attributes(investor_params) 43 | 44 | if valid? 45 | investor.save! 46 | true 47 | else 48 | false 49 | end 50 | end 51 | 52 | def investor 53 | @investor ||= Investor.new 54 | end 55 | 56 | private 57 | 58 | def investor_params 59 | params.require(:investor).permit(:id, :name, :phone_number, :email, :password, :occupation, :birthday, :red_mark, 60 | :investor_group_id, :accredited_investor, :itin_ssn, :box_url, :connected, 61 | :wire_transfer_fee, :avatar, :mail_validation, :inactive, 62 | address_attributes: %i[id attention institution address_1 address_2 address_3 city 63 | state postal_code country investor_id], 64 | inactive_investor_contact_attributes: %i[investor_id name email phone_number comments]) 65 | end 66 | 67 | def investor_params_array 68 | [:id, :name, :phone_number, :email, :password, :occupation, :birthday, :red_mark, 69 | :investor_group_id, :accredited_investor, :itin_ssn, :box_url, :connected, 70 | :wire_transfer_fee, :avatar, :email_validation, :inactive] 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /app/helpers/lead_helper.rb: -------------------------------------------------------------------------------- 1 | module LeadHelper 2 | 3 | def lead_price(listing) 4 | current = CalculateCurrency.call(price: listing.price, currency: @current_currency) 5 | delimiter = I18n.default_locale == :en ? ',' : ' ' 6 | price = number_with_delimiter(current[:price], delimiter: delimiter) 7 | if listing.price.present? && listing.price.positive? 8 | sanitize("#{current[:currency]} #{price}") 9 | else 10 | t('home.index.negotiable') 11 | end 12 | end 13 | 14 | def lead_whatsapp_link(phone_number) 15 | "https://api.whatsapp.com/send?phone=#{phone_number.phone}" 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /app/helpers/partners_helper.rb: -------------------------------------------------------------------------------- 1 | module PartnersHelper 2 | 3 | def partner_activation(partner) 4 | partner.active? ? t('partners.extend_activation') : t('partners.activation') 5 | end 6 | 7 | def partner_info?(partner) 8 | partner.address.present? || partner.website.present? 9 | end 10 | 11 | def partner_edit_page_title(redirect_url) 12 | redirect_url.present? ? I18n.t('partners.upgrade_my_profile') : I18n.t('partners.edit_my_profile') 13 | end 14 | 15 | def available_hours 16 | (0..23).map { |h| "#{h}:00" } 17 | end 18 | 19 | def logo_title(logo) 20 | logo.present? ? I18n.t('partners.form.change_logo') : I18n.t('partners.form.add_logo') 21 | end 22 | 23 | def get_premium_title(partner) 24 | partner.active? ? t('partners.extend_premium_account') : t('dashboard.menu.get_premium_account') 25 | end 26 | 27 | def working_hours(partner) 28 | work_html = '' 29 | partner.working_hours.each do |schedule| 30 | next unless schedule.completely_filled? 31 | work_html += "
  • #{schedule.start_day}-#{schedule.end_day} #{schedule.opening_hours}-#{schedule.closing_hours}
  • " 32 | end 33 | work_html 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /app/models/investor.rb: -------------------------------------------------------------------------------- 1 | class Investor < ApplicationRecord 2 | # == Constants ============================================================ 3 | 4 | # == Attributes =========================================================== 5 | 6 | # == Extensions =========================================================== 7 | enum payment_method: [:wire_transfer, :check, :no_data] 8 | enum investor_type: [:regular, :admin] 9 | 10 | include PgSearch 11 | include InvestorPerformer 12 | 13 | devise :database_authenticatable, :registerable, :recoverable, :rememberable, 14 | :trackable, :timeoutable, timeout_in: 15.minutes 15 | 16 | mount_uploader :avatar, AvatarUploader 17 | 18 | # == Relationships ======================================================== 19 | has_one :wire_transfer, dependent: :destroy 20 | has_one :check, dependent: :destroy 21 | has_one :address, dependent: :destroy 22 | has_one :pie_chart, through: :investor_dashboard 23 | has_one :inactive_investor_contact, dependent: :destroy 24 | 25 | # his viewers, managers, main connected investor 26 | has_many :investor_viewers, dependent: :destroy 27 | # investors he manages, views, his connected investors 28 | has_many :viewer_investors, class_name: 'InvestorViewer', foreign_key: :viewer_id, dependent: :destroy 29 | has_many :investor_dashboards, dependent: :destroy 30 | has_many :documents, dependent: :destroy 31 | has_many :admin_distributions, dependent: :destroy 32 | has_many :equity_datas, dependent: :destroy 33 | has_many :note_datas, dependent: :destroy 34 | has_many :opportunities_reports, dependent: :destroy 35 | has_many :notes, through: :note_datas, source: :note 36 | has_many :equities, -> { distinct }, through: :equity_datas, source: :equity 37 | has_many :viewers, through: :investor_viewers, source: :viewer 38 | has_many :opportunities, through: :opportunities_reports, source: :opportunity 39 | has_many :related_investors, dependent: :destroy 40 | has_many :backwards_related_investors, class_name: 'RelatedInvestor', 41 | foreign_key: 'related_investor_id', dependent: :destroy 42 | 43 | belongs_to :investor_group, optional: true 44 | 45 | accepts_nested_attributes_for :address, :inactive_investor_contact 46 | 47 | # == Validations ========================================================== 48 | validates :name, :phone_number, presence: true 49 | 50 | validates :phone_number, format: { with: /\A\+?[\d]+[\d\-]+\z/ }, 51 | length: { in: 10..15 }, unless: proc { |investor| investor.phone_number.blank? } 52 | 53 | validates_confirmation_of :password, message: "Passwords don't match" 54 | validates :password, format: { with: /\A(?=.*?\d.*?\d)(?=.*[a-zA-Z]).{8,15}\z/, message: 'Password does not satisfy the requirements' }, allow_nil: true 55 | 56 | # == Scopes =============================================================== 57 | scope :real, -> { where(viewer: false).order(:name) } 58 | scope :viewers, -> { where(viewer: true) } 59 | 60 | pg_search_scope :search, against: %i[name], using: { 61 | tsearch: { prefix: true } 62 | } 63 | 64 | 65 | # == Callbacks ============================================================ 66 | after_initialize :default_values 67 | after_save :update_dashboard, unless: -> { changes[:sign_in_count] || changes[:investor_group_id] } 68 | after_update :update_managers, if: -> { changes[:investor_group_id] } 69 | 70 | before_destroy :update_prime_investor!, prepend: true, if: :connected? 71 | # == Class Methods ======================================================== 72 | 73 | # == Instance Methods ===================================================== 74 | 75 | def profile_updated_notification 76 | AdminMailer.investor_update_profile(self, 'Profile was updated').deliver_now 77 | end 78 | 79 | def prime_connected_investor 80 | investor_viewers.connected.first&.viewer 81 | end 82 | 83 | def reset_tutorial 84 | self.tutorial_progress['/investor'] = false 85 | self.save 86 | end 87 | 88 | def investor_dashboard 89 | investor_dashboards.dollar.first 90 | end 91 | 92 | private 93 | 94 | def email_regexp 95 | /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /app/models/investor_fat.rb: -------------------------------------------------------------------------------- 1 | class Investor < ActiveRecord::Base 2 | 3 | devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :timeoutable, timeout_in: 15.minutes 4 | 5 | after_initialize :default_values 6 | 7 | mount_uploader :avatar, AvatarUploader 8 | 9 | has_one :wire_transfer, dependent: :destroy 10 | has_one :check, dependent: :destroy 11 | has_one :address, dependent: :destroy 12 | has_many :documents, dependent: :destroy 13 | has_many :admin_distributions, dependent: :destroy 14 | has_many :investor_viewers, dependent: :destroy 15 | has_many :investment_datas, dependent: :destroy 16 | has_many :loan_datas, dependent: :destroy 17 | has_many :opportunities_reports, dependent: :destroy 18 | 19 | has_many :loans, through: :loan_datas, source: :loan 20 | has_many :investments, through: :investment_datas, source: :investment 21 | has_many :viewers, through: :investor_viewers, source: :viewer 22 | has_many :opportunities, through: :opportunities_reports, source: :opportunity 23 | 24 | has_many :viewer_investors, class_name: 'InvestorViewer', foreign_key: :viewer_id, dependent: :destroy 25 | has_many :notifications, as: :human 26 | 27 | belongs_to :investor 28 | belongs_to :investor_group 29 | 30 | has_many :related_investors, dependent: :destroy 31 | has_many :backwards_related_investors, class_name: 'RelatedInvestor', 32 | foreign_key: 'related_investor_id', 33 | dependent: :destroy 34 | 35 | validates :name, presence: { message: "Can't be blank"} 36 | validates :email, presence: { message: "Can't be blank"}, uniqueness: { message: "Has already been taken"}, 37 | email_format: true, if: :email_validation 38 | validates :phone_number, presence: { message: "Can't be blank"}, format: { with: /\A\+?[\d]+[\d\-]+\z/, message: 'Invalid phone number'}, 39 | length: { minimum: 10, maximum: 15, message: 'The amount of characters is between 10 and 15' } 40 | 41 | validate :email_valid 42 | validate :birthday_valid 43 | 44 | enum payment_method: [:wire_transfer, :check, :no_data] 45 | 46 | accepts_nested_attributes_for :address 47 | 48 | accepts_nested_attributes_for :related_investors, :backwards_related_investors, reject_if: proc { |a| a['related_investor_id'].blank? } 49 | 50 | scope :real, -> { where(viewer: false) } 51 | scope :distributions, -> (year, quarter, direction) { real.real.joins(:admin_distributions).where('admin_distributions.year = ? AND admin_distributions.quarter = ?', year, quarter).order ("admin_distributions.payed #{direction}, name ASC") } 52 | scope :viewers, -> { where(viewer: true) } 53 | 54 | after_update :update_managers 55 | 56 | paginates_per 20 57 | 58 | def my_manager?(investor) 59 | self.investor_group.try(:manager_ids).try(:include?, investor.id) 60 | end 61 | 62 | def managers 63 | investor_viewers.where(viewer_type: 2).map { |iv| iv.investor }.uniq 64 | end 65 | 66 | def viewed_investors 67 | ids = viewer_investors.map(&:investor_id) 68 | ids << id unless self.viewer? 69 | Investor.where(id: ids) 70 | end 71 | 72 | def regular_viewers 73 | self.investor_viewers.where(viewer_type: 0).map(&:viewer) 74 | end 75 | 76 | def country 77 | address.try(:country) 78 | end 79 | 80 | def is_connected?(investor) 81 | email == investor.email && id != investor.id && investor.connected? 82 | end 83 | 84 | def uniq_email? 85 | email_validation 86 | end 87 | 88 | def has_connected_investor? 89 | Investor.where(email: email).where(connected: true).any? ? true : false 90 | end 91 | 92 | def birthday_valid 93 | if birthday.present? && (birthday <= 125.years.ago || birthday > Time.now) 94 | errors.add(:birthday, ' is invalid') 95 | end 96 | end 97 | 98 | def regular_investors 99 | InvestorViewer.where(viewer_id: id, viewer_type: 0) 100 | end 101 | 102 | def sub_investors 103 | InvestorViewer.where(viewer_id: id, viewer_type: 1) 104 | end 105 | 106 | def managed_investors 107 | InvestorViewer.where(viewer_id: id, viewer_type: 2) 108 | end 109 | 110 | def managed_group_name 111 | InvestorGroup.select { |ig| ig.manager_ids.include?(id) }.first.name 112 | end 113 | 114 | def sub_investor? 115 | Investor.select { |i| i.email == email && i.created_at < created_at }.present? 116 | end 117 | 118 | def total(value, type, action = :sum) 119 | investment_type = type == :development ? 0 : 1 120 | investment_datas.joins(:investment).where(['investments.investment_type = ?', investment_type]) 121 | .send(action, "investment_data.#{value}") 122 | end 123 | 124 | def total_final_distribution(type) 125 | result = 0 126 | investments.includes(:investment_datas).send(type).each do |investment| 127 | investment_data = investment.investment_datas.detect { |inv_data| inv_data.investor_id == id } 128 | final_distribution = investment_data.final_distribution_after_fee 129 | result += final_distribution if final_distribution 130 | end 131 | result 132 | end 133 | 134 | def default_values 135 | self.payment_method ||= :no_data 136 | self.wire_transfer_fee ||= 20 137 | end 138 | 139 | def raw_payment_method 140 | Investor.payment_methods[self.payment_method] 141 | end 142 | 143 | def different_investments? 144 | self.investments.map(&:investment_type).uniq.count > 1 145 | end 146 | 147 | def box 148 | if box_url.present? 149 | box_url.include?('http') ? box_url : 'http://' + box_url 150 | else 151 | nil 152 | end 153 | end 154 | 155 | def total_distribution(investment) 156 | investment_data = investment_datas.find { |investment_data| investment_data.investment_id == investment.id } 157 | if investment.development? 158 | if investment.distribution 159 | investment_data.invest_amount + investment_data.holding.round(2) * investment.distribution / 100 160 | else 161 | 0 162 | end 163 | elsif investment.multi_family? 164 | investment.distribution_years.flat_map(&:distributions).map do |distribution| 165 | distribution.distribution_after_holding(investment_data) 166 | end.reject(&:nil?).inject(&:+) 167 | elsif investment.loan? 168 | loan_datas.find { |loan_data| loan_data.investment_id == investment.id }.loan_dividends.map(&:amount).inject(&:+) 169 | end 170 | end 171 | 172 | end 173 | -------------------------------------------------------------------------------- /app/models/loan.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: loans 4 | # 5 | # id :integer not null, primary key 6 | # name :string 7 | # email :string 8 | # car_type :integer 9 | # amount_of_money :string 10 | # monthly_income :string 11 | # currency :string 12 | # city_id :integer 13 | # created_at :datetime not null 14 | # updated_at :datetime not null 15 | # 16 | 17 | class Loan < ApplicationRecord 18 | 19 | # == Constants ============================================================ 20 | 21 | # == Attributes =========================================================== 22 | 23 | # == Extensions =========================================================== 24 | enum car_type: %i[new_car second_hand import] 25 | 26 | # == Relationships ======================================================== 27 | belongs_to :city 28 | has_one :phone_number, as: :phoneable, dependent: :destroy, inverse_of: :phoneable 29 | 30 | accepts_nested_attributes_for :phone_number 31 | 32 | # == Validations ========================================================== 33 | validates :name, :car_type, :currency, :city_id, presence: true 34 | validates :email, presence: true, format: { with: Devise.email_regexp } 35 | validates :amount_of_money, :monthly_income, presence: true, 36 | numericality: { only_integer: true, greater_than: 0 }, 37 | length: { maximum: 10 } 38 | 39 | # == Scopes =============================================================== 40 | 41 | # == Callbacks ============================================================ 42 | 43 | # == Class Methods ======================================================== 44 | 45 | # == Instance Methods ===================================================== 46 | 47 | end 48 | -------------------------------------------------------------------------------- /app/models/report.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: reports 4 | # 5 | # id :integer not null, primary key 6 | # subject :integer not null 7 | # comment :string 8 | # listing_id :integer 9 | # email :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | class Report < ApplicationRecord 15 | 16 | # == Constants ============================================================ 17 | 18 | # == Attributes =========================================================== 19 | 20 | # == Extensions =========================================================== 21 | enum subject: %i[sold scam wrong_number] 22 | 23 | # == Relationships ======================================================== 24 | belongs_to :listing 25 | 26 | # == Validations ========================================================== 27 | validates :subject, presence: true 28 | 29 | # == Scopes =============================================================== 30 | 31 | # == Callbacks ============================================================ 32 | 33 | # == Class Methods ======================================================== 34 | 35 | # == Instance Methods ===================================================== 36 | 37 | end 38 | -------------------------------------------------------------------------------- /app/models/vehicle_listing.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: listings 4 | # 5 | # id :integer not null, primary key 6 | # new :boolean 7 | # car_type :integer 8 | # condition :integer 9 | # place :string 10 | # gear_type :integer 11 | # model_id :integer 12 | # fuel_id :integer 13 | # body_id :integer 14 | # price :integer 15 | # mileage :integer 16 | # certified :boolean 17 | # description :text 18 | # created_at :datetime not null 19 | # updated_at :datetime not null 20 | # city_id :integer 21 | # validated :boolean default(FALSE) 22 | # wheel :integer 23 | # climatisation :boolean 24 | # slug :string 25 | # color_id :integer 26 | # sold :boolean default(FALSE), not null 27 | # year :integer 28 | # engine_capacity :string 29 | # on_homepage_ended_at :datetime 30 | # fixed_on_top_ended_at :datetime 31 | # bumped_on_top_ended_at :datetime 32 | # position :integer 33 | # last_reacted :date 34 | # token :string 35 | # validated_at :date 36 | # seller_type :string 37 | # seller_id :integer 38 | # type :string 39 | # spare_part_category_id :integer 40 | # impressions_count :integer default(0) 41 | # 42 | 43 | class VehicleListing < Listing 44 | 45 | # == Constants ============================================================ 46 | 47 | # == Attributes =========================================================== 48 | attr_accessor :brand_id 49 | 50 | # == Extensions =========================================================== 51 | 52 | friendly_id :url, use: %i[slugged finders] 53 | 54 | enum car_type: { 55 | car: 1, 56 | moto: 2, 57 | truck: 3 58 | } 59 | 60 | enum gear_type: { 61 | manual: 1, 62 | automatic: 2 63 | } 64 | 65 | enum wheel: { 66 | right: 1, 67 | left: 2 68 | } 69 | 70 | # == Relationships ======================================================== 71 | belongs_to :model 72 | belongs_to :fuel 73 | belongs_to :color 74 | belongs_to :body_type, foreign_key: :body_id 75 | 76 | has_one :brand, through: :model 77 | 78 | # == Validations ========================================================== 79 | validates :price, inclusion: { in: proc { Calculate::DefaultPriceRange.call } } 80 | validates :year, inclusion: { in: 1900..Time.zone.today.year }, allow_blank: true 81 | 82 | validates :engine_capacity, :mileage, numericality: { greater_than: 0 }, allow_blank: true 83 | 84 | validates :mileage, length: { maximum: 7 } 85 | validates :description, length: { maximum: 1000 } 86 | 87 | validates :car_type, :color_id, :gear_type, :fuel_id, :body_id, :model_id, presence: true 88 | 89 | # == Scopes =============================================================== 90 | scope :color, ->(color_ids) { where(color: color_ids) } 91 | scope :wheel, ->(wheel_ids) { where(wheel: wheel_ids) } 92 | scope :model_id, ->(model_ids) { where(model_id: model_ids) } 93 | scope :body_type, ->(body_type_id) { where(body_id: body_type_id) } 94 | scope :body_id, ->(body_type_id) { where(body_id: body_type_id) } 95 | scope :car_type, ->(car_type_ids) { where(car_type: car_type_ids) } 96 | scope :fuel_id, ->(fuel_type_ids) { where(fuel_id: fuel_type_ids) } 97 | scope :maxmileage, ->(mileage) { where("mileage <= ? OR mileage = '0' OR mileage IS NULL", mileage) } 98 | scope :minmileage, ->(mileage) { where("mileage >= ? OR mileage = '0' OR mileage IS NULL", mileage) } 99 | scope :gear_type, ->(gear_type_ids) { where(gear_type: gear_type_ids) } 100 | scope :type, ->(car_type) { where(car_type: VehicleListing.car_types[car_type]) } 101 | scope :make, ->(name) { joins(:model).where('models.name = ?', name) } 102 | scope :brand, ->(name) { joins(:brand).where('brands.name = ?', name) } 103 | scope :brand_id, ->(brand_ids) { joins(:brand).where(brands: { id: brand_ids }) } 104 | scope :body, ->(name) { joins(body_type: :translations).where('body_type_translations.name = ?', name) } 105 | 106 | # == Callbacks ============================================================ 107 | has_secure_token :token 108 | 109 | # == Class Methods ======================================================== 110 | def self.prices 111 | validated.pluck(:price, :discount_price).map { |p| p.last || p.first }.compact 112 | end 113 | 114 | def self.max_price(currency) 115 | CalculateCurrency.call(price: prices.max, currency: currency)[:price] 116 | end 117 | 118 | def self.promoted_on_top(car_type, limit = ADS_ON_TOP) 119 | listings = validated.available.car_type(car_type) 120 | listings.on_promotion('fixed_on_top') 121 | .or(listings.on_promotion('on_homepage')).order('RANDOM()').limit(limit) 122 | end 123 | 124 | # == Instance Methods ===================================================== 125 | # Slug generation rule 126 | 127 | def url 128 | I18n.t('common.ad_url', locale: I18n.default_locale, model_brand_name: brand&.name, model_name: model&.name, 129 | city_region_name: region&.name, city_name: city&.name, id: id) 130 | end 131 | 132 | private 133 | 134 | def clear_unused_moto_fields 135 | return unless moto? 136 | 137 | self.climatisation = nil 138 | self.wheel = nil 139 | end 140 | 141 | end 142 | -------------------------------------------------------------------------------- /app/performers/investment_data_performer.rb: -------------------------------------------------------------------------------- 1 | module InvestmentDataPerformer 2 | def self.included(base) 3 | base.extend ClassMethods 4 | end 5 | 6 | module ClassMethods 7 | 8 | end 9 | 10 | def total_distribution 11 | investment.rental_properties? ? total_quarterly_distribution : total_distribution_dev 12 | end 13 | 14 | def profit_less_reserves_for_distribution 15 | return unless investment.profit_less_reserves_for_distribution 16 | 17 | investment.profit_less_reserves_for_distribution * holding / 100 18 | end 19 | 20 | def non_final_profit 21 | return nil unless investment.non_final_profit 22 | 23 | investment.non_final_profit * holding / 100 24 | end 25 | 26 | def final_distribution 27 | investment.distribution ? investment.distribution * holding / 100 : nil 28 | end 29 | 30 | def final_distribution_after_fee 31 | investment.distribution * holding / 100 * (100 - dragonfly_fee) / 100 if investment.distribution && dragonfly_fee 32 | end 33 | 34 | 35 | def last_distribution 36 | investment.distribution_years.try(:last).try(:distributions).try(:last).try(:amount) 37 | end 38 | 39 | def total_quarterly_distribution 40 | ((investment.total_distribution || 0) * holding / 100) || 0 41 | end 42 | 43 | def sale_distribution 44 | (distribution_from_sale || (investment.net_available_for_distribution || 0) * holding / 100) || 0 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /app/performers/investment_performer.rb: -------------------------------------------------------------------------------- 1 | module InvestmentPerformer 2 | 3 | def self.included(base) 4 | base.extend ClassMethods 5 | end 6 | 7 | module ClassMethods 8 | def sfrp 9 | Investment.single_family_rental_properties 10 | end 11 | end 12 | 13 | def total_distribution 14 | distribution_years.joins(:distributions).sum('distributions.amount') 15 | end 16 | 17 | def progresses_with_order 18 | progresses.order(development? ? 'progresses.updated DESC' : 'progresses.year DESC, progresses.quarter DESC') 19 | end 20 | 21 | def sfrp? 22 | single_family_rental_properties? 23 | end 24 | 25 | def rental_properties? 26 | multi_family? || sfrp? 27 | end 28 | 29 | def total_distribution_for_year(year) 30 | distribution_years.where(['year = ?', year]).joins(:distributions).sum('distributions.amount') 31 | end 32 | 33 | def next_image_order 34 | last_image = images.order(order: :asc).last 35 | last_image ? last_image.order + 1 : 0 36 | end 37 | 38 | def reconstruct_order 39 | images.order(order: :asc).each_with_index do |image, index| 40 | image.order = index unless image.order == index 41 | image.save 42 | end 43 | end 44 | 45 | def table_color 46 | completed? ? 'green' : 'blue' 47 | end 48 | 49 | def progress_quarters 50 | output = [] 51 | progresses.each do |progress| 52 | quarter = CalendarQuarter.new(progress.raw_quarter + 1, progress.year) 53 | output << quarter unless output.include? quarter 54 | end 55 | output.sort.reverse 56 | end 57 | 58 | def full_name 59 | complex_name.present? ? name.to_s + ' - ' + complex_name.to_s : name 60 | end 61 | 62 | def current_stage?(stage) 63 | stage.stage.include?(timeline) 64 | end 65 | 66 | def current_stage 67 | stages.find_by(stage: Investment.timelines[timeline]) 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /app/performers/investor_performer.rb: -------------------------------------------------------------------------------- 1 | module InvestorPerformer 2 | 3 | def self.included(base) 4 | base.extend ClassMethods 5 | end 6 | 7 | module ClassMethods 8 | 9 | end 10 | 11 | def sub_investor? 12 | Investor.select { |i| i.email == email && i.created_at < created_at }.present? 13 | end 14 | 15 | def default_values 16 | self.payment_method ||= :no_data 17 | self.wire_transfer_fee ||= 20 18 | end 19 | 20 | def raw_payment_method 21 | Investor.payment_methods[payment_method] 22 | end 23 | 24 | def different_investments? 25 | investments.map(&:investment_type).uniq.count >= 1 26 | end 27 | 28 | def box 29 | return nil if box_url.blank? 30 | 31 | box_url.include?('http') ? box_url : 'http://' + box_url 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /app/performers/loan_data_performer.rb: -------------------------------------------------------------------------------- 1 | module LoanDataPerformer 2 | def self.included(base) 3 | base.extend ClassMethods 4 | end 5 | 6 | module ClassMethods 7 | 8 | end 9 | 10 | def end_date 11 | ([end_of_loan] + loan_date_extensions.pluck(:maturity_date_after_extension).compact).last 12 | end 13 | 14 | def nearest_end_date 15 | if loan_date_extensions.any? 16 | lde = loan_date_extensions.pluck(:maturity_date_after_extension).unshift(end_of_loan) 17 | lde.each { |date| return date if date > Date.today } 18 | lde[-1] 19 | else 20 | end_of_loan 21 | end 22 | end 23 | 24 | def dividends 25 | dividends = loan_dividends.select { |ld| ld.submited }.map(&:amount).inject { |sum, x| sum.to_f + x.to_f } 26 | dividends.round if dividends.present? 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /app/policies/cart_policy.rb: -------------------------------------------------------------------------------- 1 | class CartPolicy < ApplicationPolicy 2 | 3 | def show? 4 | user_active? 5 | end 6 | 7 | def create? 8 | user_active? 9 | end 10 | 11 | def update? 12 | user_active? && record.waiting_for_order_details? 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /app/policies/post_policy.rb: -------------------------------------------------------------------------------- 1 | class PostPolicy < ApplicationPolicy 2 | 3 | def create? 4 | user_active? 5 | end 6 | 7 | def destroy? 8 | user_active? && current_user.posts.include?(record) 9 | end 10 | 11 | def like? 12 | user_active? && Like.find_by(post: record, user: current_user).blank? 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /app/policies/store_front_policy.rb: -------------------------------------------------------------------------------- 1 | class StorePolicy < ApplicationPolicy 2 | 3 | def show? 4 | record.operational? 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/presenters/dividend_year_dispatcher.rb: -------------------------------------------------------------------------------- 1 | class DividendYear 2 | class DividendYearDispatcher 3 | 4 | def initialize(dividend_year:, default_presenter: YearlyDistributionPresenter) 5 | @dividend_year = dividend_year 6 | @default_presenter = default_presenter 7 | end 8 | 9 | def results 10 | presenter.dividends_year 11 | end 12 | 13 | def presenter 14 | presenter_class(@dividend_year.distribution).new(@dividend_year) 15 | end 16 | 17 | def presenter_class(distribution) 18 | ('DividendYear::' + "#{distribution.gsub(/[ +]/,'_')}_distribution_presenter".camelize).safe_constantize || @default_presenter 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/presenters/monthly_distribution_presenter.rb: -------------------------------------------------------------------------------- 1 | class DividendYear 2 | class MonthlyDistributionPresenter < DividendYear::BaseDividendYearPresenter 3 | MONTHS = %w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec].freeze 4 | 5 | def dividends_year 6 | MONTHS.map do |month| 7 | value_decorator(select_dividend_by_month(@dividend_year, month).try(:amount)) 8 | end 9 | end 10 | 11 | def select_dividend_by_month(dividend_year, month) 12 | dividend_year.loan_dividends.select(&:submited).find { |ld| ld.submitted_date.strftime('%b') == month } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/presenters/quarterly_distribution_presenter.rb: -------------------------------------------------------------------------------- 1 | class DividendYear 2 | class QuarterlyDistributionPresenter < DividendYear::BaseDividendYearPresenter 3 | QUARTERS = %w[Q1 Q2 Q3 Q4].freeze 4 | 5 | def dividends_year 6 | QUARTERS.map do |quarter| 7 | value_decorator(select_dividend_by_quarter(@dividend_year, quarter).try(:amount)) 8 | end 9 | end 10 | 11 | def select_dividend_by_quarter(dividend_year, quarter) 12 | dividend_year.loan_dividends.select(&:submited).find do|ld| 13 | quarter[1] == CalendarQuarter.from_date(ld.submitted_date).quarter.to_s 14 | end 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/presenters/yearly_distribution_presenter.rb: -------------------------------------------------------------------------------- 1 | class DividendYear 2 | class YearlyDistributionPresenter < DividendYear::BaseDividendYearPresenter 3 | def dividends_year 4 | [value_decorator(@dividend_year.loan_dividends.sample.try(:amount))] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/serializers/chat_attachment_serializer.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: chat_attachments 4 | # 5 | # id :bigint not null, primary key 6 | # attachment_data :text 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # chat_message_id :bigint 10 | # 11 | # Indexes 12 | # 13 | # index_chat_attachments_on_chat_message_id (chat_message_id) 14 | # 15 | class ChatAttachmentSerializer 16 | 17 | include JSONAPI::Serializer 18 | 19 | attributes :id 20 | 21 | attribute :url, &:attachment_url 22 | 23 | attribute :type do |obj| 24 | obj.attachment.mime_type 25 | end 26 | 27 | attribute :name do |obj| 28 | obj.attachment['filename'] 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /app/serializers/chat_message_serializer.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: chat_messages 4 | # 5 | # id :bigint not null, primary key 6 | # messageble_type :string 7 | # text :text 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # chat_id :bigint 11 | # messageble_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_chat_messages_on_chat_id (chat_id) 16 | # index_chat_messages_on_messageble (messageble_type,messageble_id) 17 | # 18 | class ChatMessageSerializer 19 | 20 | include JSONAPI::Serializer 21 | 22 | attributes :id, :messageble_type, :messageble_id, :sender_full_name, :text, :created_at 23 | 24 | has_many :attachments, serializer: ChatAttachmentSerializer 25 | 26 | end 27 | -------------------------------------------------------------------------------- /app/serializers/review_serializer.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: reviews 4 | # 5 | # id :bigint not null, primary key 6 | # comment :text 7 | # rating :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # user_id :bigint 11 | # 12 | # Indexes 13 | # 14 | # index_reviews_on_user_id (user_id) 15 | # 16 | class ReviewSerializer 17 | 18 | include JSONAPI::Serializer 19 | 20 | attributes :id, :comment, :rating, :created_at 21 | 22 | belongs_to :user 23 | 24 | end 25 | -------------------------------------------------------------------------------- /app/services/calculate_currency.rb: -------------------------------------------------------------------------------- 1 | class CalculateCurrency < BaseService 2 | 3 | attr_reader :price, :currency, :rewert 4 | 5 | def initialize(options = {}) 6 | @price = options[:price].to_i 7 | @currency = options[:currency] 8 | @rewert = options[:rewert] || nil 9 | end 10 | 11 | def call 12 | rate = get_rate(ExchangeRate.base_currency, Settings.currencies[currency]) 13 | money = rate.present? ? get_price(rate) : price 14 | { currency: currency, price: money } 15 | end 16 | 17 | private 18 | 19 | def get_price(rate) 20 | rewert.present? ? (price / rate).floor : (rate * price).floor 21 | end 22 | 23 | def get_rate(from, to) 24 | Money.default_bank.get_rate(from, to) 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /app/services/connect_listings_to_seller.rb: -------------------------------------------------------------------------------- 1 | class ConnectListingsToSeller < BaseService 2 | 3 | attr_reader :resource, :listings_ids 4 | 5 | def initialize(resource, listings_ids) 6 | @resource = resource 7 | @listings_ids = listings_ids 8 | end 9 | 10 | def call 11 | return if !resource.persisted? || listings_ids.nil? 12 | connect_listings_to_seller 13 | end 14 | 15 | private 16 | 17 | def connect_listings_to_seller 18 | Listing.where(id: listings_ids).each { |listing| listing.update(seller: resource) if listing.seller.blank? } 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /app/workers/check_ad_worker.rb: -------------------------------------------------------------------------------- 1 | class CheckAdWorker 2 | 3 | include Sidekiq::Worker 4 | sidekiq_options queue: ENV['CARRIERWAVE_BACKGROUNDER_QUEUE'].to_sym 5 | 6 | def perform(ad_id) 7 | @ad = Listing.find(ad_id) 8 | return if @ad.blank? 9 | mark_as_sold if not_reacted? 10 | return if @ad.sold? 11 | send_email 12 | CheckAdWorker.perform_in(DAYS_TO_CHECK_AD.days, @ad.id) 13 | end 14 | 15 | private 16 | 17 | def mark_as_sold 18 | @ad.update_attribute('sold', true) # rubocop:disable SkipsModelValidations 19 | end 20 | 21 | def not_reacted? 22 | @ad.last_reacted < Time.zone.today - DAYS_TO_MARK_AS_SOLD 23 | end 24 | 25 | def send_email 26 | CheckAdMailer.check_email(@ad).deliver_now 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /app/workers/move_to_top_worker.rb: -------------------------------------------------------------------------------- 1 | class MoveToTopWorker 2 | 3 | include Sidekiq::Worker 4 | sidekiq_options queue: ENV.fetch('CARRIERWAVE_BACKGROUNDER_QUEUE').to_sym 5 | 6 | def perform(prom_id, prom_class) 7 | promotion = prom_class.constantize.find(prom_id) 8 | return if promotion.bumped_on_top_ended_at < Time.zone.today 9 | 10 | promotion.bump_to_top 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/workers/partner_deactivate_worker.rb: -------------------------------------------------------------------------------- 1 | class PartnerDeactivateWorker 2 | 3 | include Sidekiq::Worker 4 | sidekiq_options queue: ENV['CARRIERWAVE_BACKGROUNDER_QUEUE'].to_sym 5 | 6 | def perform(id) 7 | @partner = Partner.find(id) 8 | deactivate_partner if @partner.active_to < Time.zone.today 9 | end 10 | 11 | private 12 | 13 | def deactivate_partner 14 | @partner.update(active: false, active_to: nil) 15 | end 16 | 17 | end 18 | --------------------------------------------------------------------------------