diff --git a/.rubocop.yml b/.rubocop.yml index dfafb9e72..a7f761036 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -36,3 +36,5 @@ Style/FormatString: EnforcedStyle: sprintf Style/FormatStringToken: EnforcedStyle: template +Rails/RedundantPresenceValidationOnBelongsTo: + Enabled: false diff --git a/app/controllers/api/statistics_controller.rb b/app/controllers/api/statistics_controller.rb index 545572c9f..d657df10a 100644 --- a/app/controllers/api/statistics_controller.rb +++ b/app/controllers/api/statistics_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# API Controller for resources of type Space +# API Controller for various statistical resources (gateway to elasticsearch DB) class API::StatisticsController < API::ApiController before_action :authenticate_user! @@ -9,49 +9,25 @@ class API::StatisticsController < API::ApiController @statistics = StatisticIndex.all end - %w[account event machine project subscription training user space].each do |path| + %w[account event machine project subscription training user space order].each do |path| class_eval %{ - def #{path} - authorize :statistic, :#{path}? + def #{path} # def account + authorize :statistic, :#{path}? # authorize :statistic, :account + render json: Statistics::QueryService.query('#{path}', request) # render json: Statistics::QueryService.query('account', request) + end # end - # remove additional parameters - statistic_type = request.query_parameters.delete('stat-type') - custom_query = request.query_parameters.delete('custom-query') - start_date = request.query_parameters.delete('start-date') - end_date = request.query_parameters.delete('end-date') + def export_#{path} # def export_account + authorize :statistic, :export_#{path}? # authorize :statistic, :export_account? - # run main query in elasticSearch - query = MultiJson.load(request.body.read) - results = Stats::#{path.classify}.search(query, request.query_parameters.symbolize_keys).response - - # run additional custom aggregations, if any - CustomAggregationService.new.("#{path}", statistic_type, start_date, end_date, custom_query, results) - - # return result - render json: results - end - }, __FILE__, __LINE__ - 20 - end - - %w[account event machine project subscription training user space].each do |path| - class_eval %{ - def export_#{path} - authorize :statistic, :export_#{path}? - - export = Export.where(category:'statistics', export_type: '#{path}', query: params[:body], key: params[:type_key]).last - if export.nil? || !FileTest.exist?(export.file) - @export = Export.new(category:'statistics', - export_type: '#{path}', - user: current_user, - query: params[:body], - key: params[:type_key]) + @export = Statistics::QueryService.export('#{path}', params) # @export = Statistics::QueryService.export('account', params) + if @export.is_a?(Export) if @export.save - render json: {export_id: @export.id}, status: :ok + render json: { export_id: @export.id }, status: :ok else render json: @export.errors, status: :unprocessable_entity end else - send_file File.join(Rails.root, export.file), + send_file @export, type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment' end @@ -62,16 +38,15 @@ class API::StatisticsController < API::ApiController def export_global authorize :statistic, :export_global? - export = Export.where(category: 'statistics', export_type: 'global', query: params[:body]).last - if export.nil? || !FileTest.exist?(export.file) - @export = Export.new(category: 'statistics', export_type: 'global', user: current_user, query: params[:body]) + @export = Statistics::QueryService.export(global, params) + if @export.is_a?(Export) if @export.save render json: { export_id: @export.id }, status: :ok else render json: @export.errors, status: :unprocessable_entity end else - send_file File.join(Rails.root, export.file), + send_file @export, type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment' end diff --git a/app/controllers/api/wallet_controller.rb b/app/controllers/api/wallet_controller.rb index 75e450e55..159ee0a7e 100644 --- a/app/controllers/api/wallet_controller.rb +++ b/app/controllers/api/wallet_controller.rb @@ -18,7 +18,7 @@ class API::WalletController < API::ApiController end def credit - return head 422 unless Setting.get('wallet_module') + return head :unprocessable_entity unless Setting.get('wallet_module') @wallet = Wallet.find(credit_params[:id]) authorize @wallet @@ -28,7 +28,7 @@ class API::WalletController < API::ApiController service.create_avoir(transaction, credit_params[:avoir_date], credit_params[:avoir_description]) if credit_params[:avoir] render :show else - head 422 + head :unprocessable_entity end end diff --git a/app/frontend/src/javascript/controllers/home.js b/app/frontend/src/javascript/controllers/home.js index 8a567b736..fad9c211b 100644 --- a/app/frontend/src/javascript/controllers/home.js +++ b/app/frontend/src/javascript/controllers/home.js @@ -106,6 +106,7 @@ Application.Controllers.controller('HomeController', ['$scope', '$transition$', const setupWelcomeTour = function () { // get the tour defined by the ui-tour directive const uitour = uiTourService.getTourByName('welcome'); + if (!uitour) return; // add the steps uitour.createStep({ selector: 'body', diff --git a/app/models/order.rb b/app/models/order.rb index 0586aa81d..713e53f42 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -7,7 +7,7 @@ class Order < PaymentDocument belongs_to :coupon belongs_to :invoice has_many :order_items, dependent: :destroy - has_one :payment_gateway_object, as: :item + has_one :payment_gateway_object, as: :item, dependent: :destroy has_many :order_activities, dependent: :destroy ALL_STATES = %w[cart paid payment_failed refunded in_progress ready canceled delivered].freeze diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 04aeba681..0bc743eed 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -15,22 +15,23 @@ class Reservation < ApplicationRecord accepts_nested_attributes_for :slots_reservations, allow_destroy: true belongs_to :reservable, polymorphic: true - has_many :tickets + has_many :tickets, dependent: :destroy accepts_nested_attributes_for :tickets, allow_destroy: false has_many :invoice_items, as: :object, dependent: :destroy has_one :payment_schedule_object, as: :object, dependent: :destroy - validates_presence_of :reservable_id, :reservable_type + validates :reservable_id, :reservable_type, presence: true validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) } validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) } validate :slots_not_locked + after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' } after_commit :notify_member_create_reservation, on: :create after_commit :notify_admin_member_create_reservation, on: :create after_commit :extend_subscription, on: :create - after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' } + delegate :user, to: :statistic_profile # @param canceled if true, count the number of seats for this reservation, including canceled seats def total_booked_seats(canceled: false) @@ -50,10 +51,6 @@ class Reservation < ApplicationRecord total end - def user - statistic_profile.user - end - def update_event_nb_free_places return unless reservable_type == 'Event' @@ -83,11 +80,11 @@ class Reservation < ApplicationRecord daily_slots[1..].each do |slot| found = false result[date].each do |group_start, group_slots| - if slot[:start_at] === group_slots.last[:end_at] - result[date][group_start].push(slot) - found = true - break - end + next unless slot[:start_at] == group_slots.last[:end_at] + + result[date][group_start].push(slot) + found = true + break end result[date][slot[:start_at]] = [slot] unless found end diff --git a/app/models/stats/account.rb b/app/models/stats/account.rb index 85f6f5671..5292e501f 100644 --- a/app/models/stats/account.rb +++ b/app/models/stats/account.rb @@ -1,6 +1,7 @@ -module Stats - class Account - include Elasticsearch::Persistence::Model - include StatConcern - end +# frozen_string_literal: true + +# This is a statistical data saved in ElasticSearch, about an account creation +class Stats::Account + include Elasticsearch::Persistence::Model + include StatConcern end diff --git a/app/models/stats/event.rb b/app/models/stats/event.rb index 9ad73a6a7..9de4440b2 100644 --- a/app/models/stats/event.rb +++ b/app/models/stats/event.rb @@ -1,12 +1,13 @@ -module Stats - class Event - include Elasticsearch::Persistence::Model - include StatConcern - include StatReservationConcern +# frozen_string_literal: true - attribute :eventId, Integer - attribute :eventDate, String - attribute :ageRange, String - attribute :eventTheme, String - end +# This is a statistical data saved in ElasticSearch, about an event reservation +class Stats::Event + include Elasticsearch::Persistence::Model + include StatConcern + include StatReservationConcern + + attribute :eventId, Integer + attribute :eventDate, String + attribute :ageRange, String + attribute :eventTheme, String end diff --git a/app/models/stats/order.rb b/app/models/stats/order.rb new file mode 100644 index 000000000..9e41e1af7 --- /dev/null +++ b/app/models/stats/order.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# This is a statistical data saved in ElasticSearch, about a store's order +class Stats::Order + include Elasticsearch::Persistence::Model + include StatConcern + + attribute :orderId, Integer + attribute :state, String + attribute :products, Array + attribute :categories, Array + attribute :ca, Float +end diff --git a/app/models/stats/project.rb b/app/models/stats/project.rb index d1ecf9fad..d97668a96 100644 --- a/app/models/stats/project.rb +++ b/app/models/stats/project.rb @@ -1,14 +1,15 @@ -module Stats - class Project - include Elasticsearch::Persistence::Model - include StatConcern +# frozen_string_literal: true - attribute :projectId, Integer - attribute :name, String - attribute :licence, Hash - attribute :themes, Array - attribute :components, Array - attribute :machines, Array - attribute :users, Integer - end +# This is a statistical data saved in ElasticSearch, about a project publication +class Stats::Project + include Elasticsearch::Persistence::Model + include StatConcern + + attribute :projectId, Integer + attribute :name, String + attribute :licence, Hash + attribute :themes, Array + attribute :components, Array + attribute :machines, Array + attribute :users, Integer end diff --git a/app/models/stats/space.rb b/app/models/stats/space.rb index fda08fa18..62b1e6aa7 100644 --- a/app/models/stats/space.rb +++ b/app/models/stats/space.rb @@ -1,9 +1,10 @@ -module Stats - class Space - include Elasticsearch::Persistence::Model - include StatConcern - include StatReservationConcern +# frozen_string_literal: true - attribute :spaceId, Integer - end +# This is a statistical data saved in ElasticSearch, about a space reservation +class Stats::Space + include Elasticsearch::Persistence::Model + include StatConcern + include StatReservationConcern + + attribute :spaceId, Integer end diff --git a/app/models/stats/subscription.rb b/app/models/stats/subscription.rb index 7fef5fd2f..2d60429c3 100644 --- a/app/models/stats/subscription.rb +++ b/app/models/stats/subscription.rb @@ -1,12 +1,13 @@ -module Stats - class Subscription - include Elasticsearch::Persistence::Model - include StatConcern +# frozen_string_literal: true - attribute :ca, Float - attribute :planId, Integer - attribute :subscriptionId, Integer - attribute :invoiceItemId, Integer - attribute :groupName, String - end +# This is a statistical data saved in ElasticSearch, about a subscription to a plan +class Stats::Subscription + include Elasticsearch::Persistence::Model + include StatConcern + + attribute :ca, Float + attribute :planId, Integer + attribute :subscriptionId, Integer + attribute :invoiceItemId, Integer + attribute :groupName, String end diff --git a/app/models/stats/training.rb b/app/models/stats/training.rb index 79854fecb..3c877e9f8 100644 --- a/app/models/stats/training.rb +++ b/app/models/stats/training.rb @@ -1,10 +1,11 @@ -module Stats - class Training - include Elasticsearch::Persistence::Model - include StatConcern - include StatReservationConcern +# frozen_string_literal: true - attribute :trainingId, Integer - attribute :trainingDate, String - end +# This is a statistical data saved in ElasticSearch, about a training reservation +class Stats::Training + include Elasticsearch::Persistence::Model + include StatConcern + include StatReservationConcern + + attribute :trainingId, Integer + attribute :trainingDate, String end diff --git a/app/models/stats/user.rb b/app/models/stats/user.rb index 0720844d0..55c9a42a8 100644 --- a/app/models/stats/user.rb +++ b/app/models/stats/user.rb @@ -1,6 +1,7 @@ -module Stats - class User - include Elasticsearch::Persistence::Model - include StatConcern - end +# frozen_string_literal: true + +# This is a statistical data saved in ElasticSearch, about revenue generated per user +class Stats::User + include Elasticsearch::Persistence::Model + include StatConcern end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 5c0e40dd1..0754fa515 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -8,11 +8,11 @@ class Subscription < ApplicationRecord belongs_to :statistic_profile has_one :payment_schedule_object, as: :object, dependent: :destroy - has_one :payment_gateway_object, as: :item + has_one :payment_gateway_object, as: :item, dependent: :destroy has_many :invoice_items, as: :object, dependent: :destroy has_many :offer_days, dependent: :destroy - validates_presence_of :plan_id + validates :plan_id, presence: true validates_with SubscriptionGroupValidator # creation @@ -21,18 +21,21 @@ class Subscription < ApplicationRecord after_save :notify_admin_subscribed_plan after_save :notify_partner_subscribed_plan, if: :of_partner_plan? + delegate :user, to: :statistic_profile + def generate_and_save_invoice(operator_profile_id) generate_invoice(operator_profile_id).save end def expire(time) - if !expired? - update_columns(expiration_date: time, canceled_at: time) + if expired? + false + else + # TODO, check if the rubocop:disable directove can be deleted + update_columns(expiration_date: time, canceled_at: time) # rubocop:disable Rails/SkipsModelValidations notify_admin_subscription_canceled notify_member_subscription_canceled true - else - false end end @@ -47,10 +50,6 @@ class Subscription < ApplicationRecord expiration_date end - def user - statistic_profile.user - end - def original_payment_schedule payment_schedule_object&.payment_schedule end diff --git a/app/models/wallet.rb b/app/models/wallet.rb index 3ff32dffa..e4c76a018 100644 --- a/app/models/wallet.rb +++ b/app/models/wallet.rb @@ -11,6 +11,8 @@ class Wallet < ApplicationRecord validates :invoicing_profile, presence: true + delegate :user, to: :invoicing_profile + def credit(amount) if amount.is_a?(Numeric) && amount >= 0 self.amount += amount @@ -26,8 +28,4 @@ class Wallet < ApplicationRecord end false end - - def user - invoicing_profile.user - end end diff --git a/app/models/wallet_transaction.rb b/app/models/wallet_transaction.rb index 87311ce6b..e95d08b0a 100644 --- a/app/models/wallet_transaction.rb +++ b/app/models/wallet_transaction.rb @@ -8,17 +8,15 @@ class WalletTransaction < ApplicationRecord belongs_to :wallet belongs_to :reservation # what was paid with the wallet - has_one :invoice - has_one :payment_schedule + has_one :invoice, dependent: :nullify + has_one :payment_schedule, dependent: :nullify # how the wallet was credited has_one :invoice_item, as: :object, dependent: :destroy - validates_inclusion_of :transaction_type, in: %w[credit debit] + validates :transaction_type, inclusion: { in: %w[credit debit] } validates :invoicing_profile, :wallet, presence: true - def user - invoicing_profile.user - end + delegate :user, to: :invoicing_profile def original_invoice invoice_item.invoice diff --git a/app/policies/statistic_policy.rb b/app/policies/statistic_policy.rb index 47814de04..852786a89 100644 --- a/app/policies/statistic_policy.rb +++ b/app/policies/statistic_policy.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + +# Check the access policies for API::StatisticsController class StatisticPolicy < ApplicationPolicy - %w(index account event machine project subscription training user space scroll export_subscription export_machine - export_training export_event export_account export_project export_space export_global).each do |action| + %w[index account event machine project subscription training user space order scroll export_subscription export_machine + export_training export_event export_account export_project export_space export_order export_global].each do |action| define_method "#{action}?" do user.admin? end diff --git a/app/services/export_service.rb b/app/services/export_service.rb index 1e4bd3ebe..0ffea8daa 100644 --- a/app/services/export_service.rb +++ b/app/services/export_service.rb @@ -12,6 +12,8 @@ class ExportService last_export_reservations when 'users/subscription' last_export_subscriptions + when %r{statistics/.*} + last_export_statistics(type.split('/')[1]) else raise TypeError "unknown export type: #{type}" end @@ -44,5 +46,10 @@ class ExportService .where('created_at > ?', last_update) .last end + + def last_export_statistics(type) + Export.where(category: 'statistics', export_type: type, query: params[:body], key: params[:type_key]) + .last + end end end diff --git a/app/services/orders/order_service.rb b/app/services/orders/order_service.rb index 6386ed07d..f1fdf5669 100644 --- a/app/services/orders/order_service.rb +++ b/app/services/orders/order_service.rb @@ -42,6 +42,10 @@ class Orders::OrderService else nil end + + # update in elasticsearch (statistics) + stat_order = Stats::Order.search(query: { term: { orderId: order.id } }) + stat_order.map { |s| s.update(state: state) } end def in_stock?(order, stock_type = 'external') diff --git a/app/services/statistics/builder_service.rb b/app/services/statistics/builder_service.rb index 53d7a49a5..60fcc57d2 100644 --- a/app/services/statistics/builder_service.rb +++ b/app/services/statistics/builder_service.rb @@ -11,6 +11,7 @@ class Statistics::BuilderService Statistics::Builders::ReservationsBuilderService.build(options) Statistics::Builders::MembersBuilderService.build(options) Statistics::Builders::ProjectsBuilderService.build(options) + Statistics::Builders::StoreOrdersBuilderService.build(options) end private diff --git a/app/services/statistics/builders/store_orders_builder_service.rb b/app/services/statistics/builders/store_orders_builder_service.rb new file mode 100644 index 000000000..640418591 --- /dev/null +++ b/app/services/statistics/builders/store_orders_builder_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Generate statistics indicators about store's orders +class Statistics::Builders::StoreOrdersBuilderService + include Statistics::Concerns::HelpersConcern + include Statistics::Concerns::StoreOrdersConcern + + class << self + def build(options = default_options) + states = { + 'paid-processed': %w[paid in_progress ready delivered], + aborted: %w[payment_failed refunded canceled] + } + # orders list + states.each do |sub_type, order_states| + Statistics::FetcherService.store_orders_list(order_states, options).each do |o| + Stats::Order.create({ date: format_date(o[:date]), + type: 'store', + subType: sub_type, + ca: o[:ca], + products: o[:order_products], + categories: o[:order_categories], + orderId: o[:order_id], + state: o[:order_state], + stat: 1 }.merge(user_info_stat(o))) + end + end + end + end +end diff --git a/app/services/statistics/cleaner_service.rb b/app/services/statistics/cleaner_service.rb index 671700869..8ed6b50e1 100644 --- a/app/services/statistics/cleaner_service.rb +++ b/app/services/statistics/cleaner_service.rb @@ -7,7 +7,7 @@ class Statistics::CleanerService class << self def clean_stat(options = default_options) client = Elasticsearch::Model.client - %w[Account Event Machine Project Subscription Training User Space].each do |o| + %w[Account Event Machine Project Subscription Training User Space Order].each do |o| model = "Stats::#{o}".constantize client.delete_by_query( index: model.index_name, diff --git a/app/services/statistics/concerns/store_orders_concern.rb b/app/services/statistics/concerns/store_orders_concern.rb new file mode 100644 index 000000000..f5b44b77e --- /dev/null +++ b/app/services/statistics/concerns/store_orders_concern.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Provides methods to consolidate data from Store Orders to use in statistics +module Statistics::Concerns::StoreOrdersConcern + extend ActiveSupport::Concern + + class_methods do + def get_order_products(order) + order.order_items.where(orderable_type: 'Product').map do |item| + { id: item.orderable_id, name: item.orderable.name } + end + end + + def get_order_categories(order) + order.order_items + .where(orderable_type: 'Product') + .map(&:orderable) + .map(&:product_category) + .map { |cat| { id: cat.id, name: cat.name } } + .uniq + end + + def store_order_info(order) + { + order_id: order.id, + order_state: order.state, + order_products: get_order_products(order), + order_categories: get_order_categories(order) + } + end + end +end diff --git a/app/services/statistics/fetcher_service.rb b/app/services/statistics/fetcher_service.rb index 0569e9ecc..9838a343b 100644 --- a/app/services/statistics/fetcher_service.rb +++ b/app/services/statistics/fetcher_service.rb @@ -6,6 +6,7 @@ class Statistics::FetcherService include Statistics::Concerns::HelpersConcern include Statistics::Concerns::ComputeConcern include Statistics::Concerns::ProjectsConcern + include Statistics::Concerns::StoreOrdersConcern class << self def subscriptions_list(options = default_options) @@ -182,6 +183,22 @@ class Statistics::FetcherService result end + def store_orders_list(states, options = default_options) + result = [] + Order.includes(order_items: [:orderable]) + .joins(:order_items, :order_activities) + .where(order_items: { orderable_type: 'Product' }) + .where(orders: { state: states }) + .where('order_activities.created_at >= :start_date AND order_activities.created_at <= :end_date', options) + .group('orders.id') + .each do |o| + result.push({ date: o.created_at.to_date, ca: calcul_ca(o.invoice) } + .merge(user_info(o.statistic_profile)) + .merge(store_order_info(o))) + end + result + end + private def add_ca(profile, new_ca, users_list) diff --git a/app/services/statistics/query_service.rb b/app/services/statistics/query_service.rb new file mode 100644 index 000000000..9c1580486 --- /dev/null +++ b/app/services/statistics/query_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Query the elasticsearch database of statistics and format the result +class Statistics::QueryService + class << self + def query(statistic_index, request) + # remove additional parameters + statistic_type = request.query_parameters.delete('stat-type') + custom_query = request.query_parameters.delete('custom-query') + start_date = request.query_parameters.delete('start-date') + end_date = request.query_parameters.delete('end-date') + + # run main query in elasticSearch + query = MultiJson.load(request.body.read) + model = "Stats::#{statistic_index}".constantize + results = model.search(query, request.query_parameters.symbolize_keys).response + + # run additional custom aggregations, if any + CustomAggregationService.new.call(statistic_index, statistic_type, start_date, end_date, custom_query, results) + + results + end + + def export(statistic_index, params) + export = ExportService.last_export("statistics/#{statistic_index}") + if export.nil? || !FileTest.exist?(export.file) + Export.new(category: 'statistics', + export_type: statistic_index, + user: current_user, + query: params[:body], + key: params[:type_key]) + else + File.root.join(export.file) + end + end + end +end diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index 570150213..6dc4dbcdd 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -9,33 +9,33 @@ class WalletService ## credit an amount to wallet, if credit success then return a wallet transaction and notify to admin def credit(amount) + transaction = nil ActiveRecord::Base.transaction do - if @wallet.credit(amount) + if @wallet&.credit(amount) transaction = WalletTransaction.new( - invoicing_profile: @user.invoicing_profile, + invoicing_profile: @user&.invoicing_profile, wallet: @wallet, transaction_type: 'credit', amount: amount ) - if transaction.save - NotificationCenter.call type: 'notify_user_wallet_is_credited', - receiver: @wallet.user, - attached_object: transaction - NotificationCenter.call type: 'notify_admin_user_wallet_is_credited', - receiver: User.admins_and_managers, - attached_object: transaction - transaction - end + raise ActiveRecord::Rollback unless transaction.save + + NotificationCenter.call type: 'notify_user_wallet_is_credited', + receiver: @wallet&.user, + attached_object: transaction + NotificationCenter.call type: 'notify_admin_user_wallet_is_credited', + receiver: User.admins_and_managers, + attached_object: transaction end - raise ActiveRecord::Rollback end - false + transaction end ## debit an amount to wallet, if debit success then return a wallet transaction def debit(amount) + transaction = nil ActiveRecord::Base.transaction do - if @wallet.debit(amount) + if @wallet&.debit(amount) transaction = WalletTransaction.new( invoicing_profile: @user&.invoicing_profile, wallet: @wallet, @@ -43,11 +43,10 @@ class WalletService amount: amount ) - transaction if transaction.save + raise ActiveRecord::Rollback unless transaction.save end - raise ActiveRecord::Rollback end - false + transaction end ## create a refund invoice associated with the given wallet transaction diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 37dafd322..c70af01fb 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1258,6 +1258,7 @@ en: export_is_running_you_ll_be_notified_when_its_ready: "Export is running. You'll be notified when it's ready." create_plans_to_start: "Start by creating new subscription plans." click_here: "Click here to create your first one." + average_cart: "Average cart:" #statistics graphs stats_graphs: statistics: "Statistics" diff --git a/config/locales/en.yml b/config/locales/en.yml index 2ad1f5e96..785bdafa4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -430,6 +430,7 @@ en: subscriptions: "Subscriptions" machines_hours: "Machines slots" spaces: "Spaces" + orders: "Orders" trainings: "Trainings" events: "Events" registrations: "Registrations" @@ -453,6 +454,9 @@ en: account_creation: "Account creation" project_publication: "Project publication" duration: "Duration" + store: "Boutique" + paid-processed: "Paid and/or processed" + aborted: "Aborted" #statistics exports to the Excel file format export: entries: "Entries" diff --git a/config/routes.rb b/config/routes.rb index b3ed0cbb6..4759349aa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -283,7 +283,7 @@ Rails.application.routes.draw do end end - %w[account event machine project subscription training user space].each do |path| + %w[account event machine project subscription training user space order].each do |path| post "/stats/#{path}/_search", to: "api/statistics##{path}" post "/stats/#{path}/export", to: "api/statistics#export_#{path}" end diff --git a/db/seeds.rb b/db/seeds.rb index 0234e0a23..a25f979ad 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1027,6 +1027,30 @@ unless StatisticIndex.find_by(es_type_key: 'space') ]) end +unless StatisticIndex.find_by(es_type_key: 'order') + index = StatisticIndex.create!(es_type_key: 'order', label: I18n.t('statistics.orders')) + type = StatisticType.create!([ + { statistic_index_id: index.id, key: 'store', label: I18n.t('statistics.store'), + graph: true, simple: true } + ]) + StatisticSubType.create!([ + { key: 'paid-processed', label: I18n.t('statistics.paid-processed'), statistic_types: [type] }, + { key: 'aborted', label: I18n.t('statistics.aborted'), statistic_types: [type] } + ]) + + # average cart price for orders + average_cart = StatisticCustomAggregation.new( + statistic_type_id: type.id, + es_index: 'stats', + es_type: 'order', + field: 'average_cart', + query: '{"size":0, "aggregations":{"%{aggs_name}":{"avg":{"field":"ca", ' \ + '"script":"BigDecimal.valueOf(_value).setScale(1, RoundingMode.HALF_UP)", "missing": 0}}}, ' \ + '"query":{"bool":{"must":[{"range": {"date":{"gte":"%{start_date}", "lte":"%{end_date}"}}}]}}}' + ) + average_cart.save! +end + ProfileCustomField.find_or_create_by(label: 'N° SIRET') ProfileCustomField.find_or_create_by(label: 'Code NAF') ProfileCustomField.find_or_create_by(label: 'N° TVA intracommunautaire') diff --git a/test/fixtures/assets.yml b/test/fixtures/assets.yml index c4214fbcf..a9edcdd28 100644 --- a/test/fixtures/assets.yml +++ b/test/fixtures/assets.yml @@ -70,3 +70,103 @@ asset_8: type: UserAvatar created_at: 2018-12-27 14:48:52.141382000 Z updated_at: 2018-12-27 14:48:52.141382000 Z + +asset_2707: + id: 2707 + viewable_id: 3 + viewable_type: Product + attachment: appset-icon.png + type: ProductImage + created_at: '2022-09-13 10:05:07.212923' + updated_at: '2022-09-27 10:17:51.976313' + is_main: true +asset_2729: + id: 2729 + viewable_id: 16 + viewable_type: Product + attachment: osb.jpeg + type: ProductImage + created_at: '2022-09-14 13:50:12.588896' + updated_at: '2022-09-27 10:13:39.414052' + is_main: true +asset_2727: + id: 2727 + viewable_id: 15 + viewable_type: Product + attachment: cp.jpeg + type: ProductImage + created_at: '2022-09-14 13:49:08.237136' + updated_at: '2022-09-27 10:13:51.282923' + is_main: true +asset_2721: + id: 2721 + viewable_id: 12 + viewable_type: Product + attachment: casqueapointe.jpg + type: ProductImage + created_at: '2022-09-14 13:42:36.731315' + updated_at: '2022-09-27 10:14:23.371827' + is_main: true +asset_2731: + id: 2731 + viewable_id: 17 + viewable_type: Product + attachment: 3plis.jpeg + type: ProductImage + created_at: '2022-09-14 13:51:12.634244' + updated_at: '2022-09-27 10:13:28.657539' + is_main: true +asset_2725: + id: 2725 + viewable_id: 14 + viewable_type: Product + attachment: melamine.jpeg + type: ProductImage + created_at: '2022-09-14 13:48:05.033284' + updated_at: '2022-09-27 10:14:00.569077' + is_main: true +asset_2723: + id: 2723 + viewable_id: 13 + viewable_type: Product + attachment: mdf.jpeg + type: ProductImage + created_at: '2022-09-14 13:47:18.129274' + updated_at: '2022-09-27 10:14:10.137542' + is_main: true +asset_2733: + id: 2733 + viewable_id: 18 + viewable_type: Product + attachment: lamelle-colle.jpeg + type: ProductImage + created_at: '2022-09-14 13:52:08.666869' + updated_at: '2022-09-27 10:13:10.979122' + is_main: true +asset_2719: + id: 2719 + viewable_id: 11 + viewable_type: Product + attachment: bulldozer.png + type: ProductImage + created_at: '2022-09-14 13:41:16.209363' + updated_at: '2022-09-27 10:14:33.153221' + is_main: true +asset_2743: + id: 2743 + viewable_id: 22 + viewable_type: Product + attachment: filament_pla_1_75_bleu.jpeg + type: ProductImage + created_at: '2022-10-03 14:00:50.082820' + updated_at: '2022-10-03 14:00:50.082820' + is_main: true +asset_2744: + id: 2744 + viewable_id: 23 + viewable_type: Product + attachment: filament-pla_blanc.webp + type: ProductImage + created_at: '2022-10-03 14:01:43.711513' + updated_at: '2022-10-03 14:01:43.711513' + is_main: true diff --git a/test/fixtures/invoice_items.yml b/test/fixtures/invoice_items.yml index 01cb8bcc3..fc5d8f92a 100644 --- a/test/fixtures/invoice_items.yml +++ b/test/fixtures/invoice_items.yml @@ -72,3 +72,159 @@ invoice_item_6: object_type: Subscription object_id: 4 main: true +invoice_item_11702: + id: 11702 + invoice_id: 5811 + amount: 4000 + created_at: '2022-09-20 15:14:23.015625' + updated_at: '2022-09-20 15:14:23.180166' + description: Tablette lamellé-collé x 1 + invoice_item_id: + footprint: 1d112a787e89d444f2817b909d7857a3a26fc2a3cd4c4404fb50bec282988a02 + object_type: OrderItem + object_id: 1 + main: true +invoice_item_11703: + id: 11703 + invoice_id: 5811 + amount: 500 + created_at: '2022-09-20 15:14:23.287522' + updated_at: '2022-09-20 15:14:23.289957' + description: Panneaux de MDF x 1 + invoice_item_id: + footprint: 67f143564abfeab7cb93988a02987a5a2b04a2632d0f9cfde61fa39c398cd02b + object_type: OrderItem + object_id: 2 + main: false +invoice_item_11704: + id: 11704 + invoice_id: 5812 + amount: 6000 + created_at: '2022-09-20 15:14:48.367843' + updated_at: '2022-09-20 15:14:48.385139' + description: Panneau de contre-plaqué x 4 + invoice_item_id: + footprint: ea51c614632c018f0fae53c2bf8503c71d18c082ae3246155971e19ac2db143a + object_type: OrderItem + object_id: 3 + main: true +invoice_item_11712: + id: 11712 + invoice_id: 5816 + amount: 119 + created_at: '2022-10-04 12:36:03.103560' + updated_at: '2022-10-04 12:36:03.282529' + description: Filament PLA blanc x 1 + invoice_item_id: + footprint: 4c5c8a1d7884502ea7791aeed1701cf3761562855d51ed19396b344c665c308b + object_type: OrderItem + object_id: 16 + main: true +invoice_item_11713: + id: 11713 + invoice_id: 5816 + amount: 200 + created_at: '2022-10-04 12:36:03.286776' + updated_at: '2022-10-04 12:36:03.290235' + description: Filament PLA bleu x 1 + invoice_item_id: + footprint: 12c1a2c7fdb0f4b35d0c24137e6e0bdbbb39fa5c6500aa7837c67f92084d5071 + object_type: OrderItem + object_id: 17 + main: false +invoice_item_11714: + id: 11714 + invoice_id: 5817 + amount: 119 + created_at: '2022-10-04 13:54:42.977460' + updated_at: '2022-10-04 13:54:42.992564' + description: Filament PLA blanc x 1 + invoice_item_id: + footprint: cdb07fe1f0b986b9a53b6ec71f91a96e8b8d438592e88fd0041133b2ab46c8ca + object_type: OrderItem + object_id: 18 + main: true +invoice_item_11715: + id: 11715 + invoice_id: 5817 + amount: 1500 + created_at: '2022-10-04 13:54:43.021426' + updated_at: '2022-10-04 13:54:43.024326' + description: Panneau de contre-plaqué x 1 + invoice_item_id: + footprint: 3ac08745b737bc370424ee9f1b68b421000eb98e0f3e65d7395d905d75de05c6 + object_type: OrderItem + object_id: 19 + main: false +invoice_item_11716: + id: 11716 + invoice_id: 5818 + amount: 1000 + created_at: '2022-10-04 14:04:12.745449' + updated_at: '2022-10-04 14:04:12.749266' + description: Bulldozer x 1 + invoice_item_id: + footprint: 1f4b6489579d7f045c846b97bc8ed402ec91f6fd7a5c2fa93b74eb7d21c7de39 + object_type: OrderItem + object_id: 20 + main: true +invoice_item_11717: + id: 11717 + invoice_id: 5819 + amount: 2 + created_at: '2022-10-04 14:17:52.856695' + updated_at: '2022-10-04 14:17:52.871262' + description: Sticker Hello x 2 + invoice_item_id: + footprint: 0fbecff886d9c2ffc332def13fa2c8869be8a40d2be7a3126a77974bd920c563 + object_type: OrderItem + object_id: 21 + main: true +invoice_item_11718: + id: 11718 + invoice_id: 5819 + amount: 4000 + created_at: '2022-10-04 14:17:52.875854' + updated_at: '2022-10-04 14:17:52.878780' + description: Tablette lamellé-collé x 1 + invoice_item_id: + footprint: ad160b9357518b31f63661fe38998b8c1d97fda61bc744876826ae638a8142a0 + object_type: OrderItem + object_id: 22 + main: false +invoice_item_11719: + id: 11719 + invoice_id: 5820 + amount: 12000 + created_at: '2022-10-04 14:25:37.319701' + updated_at: '2022-10-04 14:25:37.322782' + description: Tablette lamellé-collé x 3 + invoice_item_id: + footprint: 409a6f0b9f67510038a8b9a407db6d09b97f12d7169d6f6f121eedb2941d1bfc + object_type: OrderItem + object_id: 11 + main: true +invoice_item_11720: + id: 11720 + invoice_id: 5821 + amount: 12000 + created_at: '2022-10-04 14:32:28.209257' + updated_at: '2022-10-04 14:32:28.226556' + description: Tablette lamellé-collé x 3 + invoice_item_id: + footprint: 733610b9c8e33f6a63f497d867d40386c623f12a526814d09d88a96f53741b7b + object_type: OrderItem + object_id: 23 + main: true +invoice_item_11721: + id: 11721 + invoice_id: 5822 + amount: 3000 + created_at: '2022-10-04 14:35:40.603969' + updated_at: '2022-10-04 14:35:40.608505' + description: Panneau de 3 plis mélèze x 1 + invoice_item_id: + footprint: c1ad8c20d080ebc8267ff0d1b668feb8c989c2561be75d311f29f49a03405895 + object_type: OrderItem + object_id: 24 + main: true diff --git a/test/fixtures/invoices.yml b/test/fixtures/invoices.yml index 2fd5ea166..b5d0c3462 100644 --- a/test/fixtures/invoices.yml +++ b/test/fixtures/invoices.yml @@ -124,3 +124,183 @@ invoice_6: invoicing_profile_id: 8 operator_profile_id: 1 statistic_profile_id: 8 +invoice_5811: + id: 5811 + total: 4500 + created_at: '2022-09-20 15:14:22.873707' + updated_at: '2022-09-20 15:14:23.496793' + reference: '2209002' + payment_method: local + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: '094590df6330de6a2b5d2ce7230673c7178f2639ca8ceb51ba272795349fff95' + environment: development + invoicing_profile_id: 3 + operator_profile_id: 1 + statistic_profile_id: 3 +invoice_5812: + id: 5812 + total: 6000 + created_at: '2022-09-20 15:14:48.345927' + updated_at: '2022-09-20 15:14:48.409110' + reference: '2209004' + payment_method: local + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: ea5cc57a8af956f9a3e66ec1f9bd7fe5fe51e1220955cacb11a3cc99d2e6aa54 + environment: development + invoicing_profile_id: 7 + operator_profile_id: 1 + statistic_profile_id: 7 +invoice_5816: + id: 5816 + total: 319 + created_at: '2022-10-04 12:36:03.060832' + updated_at: '2022-10-04 12:36:03.445507' + reference: 2210002/VL + payment_method: card + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: ca8879f07cf1bd29500a9df29ce110843fb204ab0da8140bb4c6e8e908a65e0b + environment: development + invoicing_profile_id: 4 + operator_profile_id: 4 + statistic_profile_id: 4 +invoice_5817: + id: 5817 + total: 1296 + created_at: '2022-10-04 13:54:42.975196' + updated_at: '2022-10-04 13:54:43.070098' + reference: 2210004/VL + payment_method: card + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: 30 + footprint: 838ae505dfba54f6a69cd341abf8f97fd9370c2acb81e37c6e70d98049d80646 + environment: development + invoicing_profile_id: 4 + operator_profile_id: 4 + statistic_profile_id: 4 +invoice_5818: + id: 5818 + total: 1000 + created_at: '2022-10-04 14:04:12.742685' + updated_at: '2022-10-04 14:04:12.774844' + reference: 2210006/VL + payment_method: card + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: 93db8c6ded7066a71927b6950cf5a9ea69aff25036df3a5a4259615347f2c8b2 + environment: development + invoicing_profile_id: 4 + operator_profile_id: 4 + statistic_profile_id: 4 +invoice_5819: + id: 5819 + total: 4002 + created_at: '2022-10-04 14:17:52.854636' + updated_at: '2022-10-04 14:17:52.898044' + reference: 2210008/VL + payment_method: card + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: bfd2f78a99aadd137f3f28fe8258a46ff8afa69b458f0a6f2dce1205fab22784 + environment: development + invoicing_profile_id: 4 + operator_profile_id: 4 + statistic_profile_id: 4 +invoice_5820: + id: 5820 + total: 12000 + created_at: '2022-10-04 14:25:37.291945' + updated_at: '2022-10-04 14:25:37.341401' + reference: '2210010' + payment_method: local + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: db6a663a25e19229ee93868765e1463ec7bd7856b173c5dd326d70d98369cfc0 + environment: development + invoicing_profile_id: 3 + operator_profile_id: 1 + statistic_profile_id: 3 +invoice_5821: + id: 5821 + total: 12000 + created_at: '2022-10-04 14:32:28.204985' + updated_at: '2022-10-04 14:32:28.292591' + reference: '2210012' + payment_method: local + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: 3ed9a13e5c7155bc3b186f3215b90345cc5520822728796e7c1c00ad5128ed01 + environment: development + invoicing_profile_id: 2 + operator_profile_id: 1 + statistic_profile_id: 2 +invoice_5822: + id: 5822 + total: 3000 + created_at: '2022-10-04 14:35:40.584472' + updated_at: '2022-10-04 14:35:40.637091' + reference: '2210014' + payment_method: local + avoir_date: + invoice_id: + type: + subscription_to_expire: + description: + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: 63d069e72b4992244861e9450b0c3ba3b9b30638af972a631b81578091d2925d + environment: development + invoicing_profile_id: 2 + operator_profile_id: 1 + statistic_profile_id: 2 diff --git a/test/fixtures/order_activities.yml b/test/fixtures/order_activities.yml new file mode 100644 index 000000000..707e9d505 --- /dev/null +++ b/test/fixtures/order_activities.yml @@ -0,0 +1,129 @@ +order_activity_1: + id: 1 + order_id: 1 + operator_profile_id: + activity_type: paid + note: + created_at: '2022-09-20 15:14:22.596166' + updated_at: '2022-09-20 15:14:22.596166' +order_activity_2: + id: 2 + order_id: 2 + operator_profile_id: + activity_type: paid + note: + created_at: '2022-09-20 15:14:48.328689' + updated_at: '2022-09-20 15:14:48.328689' +order_activity_6: + id: 6 + order_id: 9 + operator_profile_id: + activity_type: paid + note: + created_at: '2022-10-04 12:36:02.719677' + updated_at: '2022-10-04 12:36:02.719677' +order_activity_7: + id: 7 + order_id: 2 + operator_profile_id: 2437 + activity_type: ready + note: Merci de venir retirer la commande + created_at: '2022-10-04 13:37:49.932146' + updated_at: '2022-10-04 13:37:49.932146' +order_activity_11: + id: 11 + order_id: 9 + operator_profile_id: 2437 + activity_type: ready + note: '' + created_at: '2022-10-04 13:50:30.609274' + updated_at: '2022-10-04 13:50:30.609274' +order_activity_12: + id: 12 + order_id: 10 + operator_profile_id: + activity_type: paid + note: + created_at: '2022-10-04 13:54:42.749983' + updated_at: '2022-10-04 13:54:42.749983' +order_activity_13: + id: 13 + order_id: 10 + operator_profile_id: 2437 + activity_type: ready + note: '' + created_at: '2022-10-04 13:54:56.790702' + updated_at: '2022-10-04 13:54:56.790702' +order_activity_14: + id: 14 + order_id: 11 + operator_profile_id: + activity_type: paid + note: + created_at: '2022-10-04 14:04:12.721800' + updated_at: '2022-10-04 14:04:12.721800' +order_activity_15: + id: 15 + order_id: 11 + operator_profile_id: 2437 + activity_type: ready + note: '' + created_at: '2022-10-04 14:04:28.489152' + updated_at: '2022-10-04 14:04:28.489152' +order_activity_16: + id: 16 + order_id: 12 + operator_profile_id: + activity_type: paid + note: + created_at: '2022-10-04 14:17:52.835587' + updated_at: '2022-10-04 14:17:52.835587' +order_activity_17: + id: 17 + order_id: 12 + operator_profile_id: 2437 + activity_type: ready + note: "
je ne sais pas que te dire mon ami
" + created_at: '2022-10-04 14:22:54.560670' + updated_at: '2022-10-04 14:22:54.560670' +order_activity_18: + id: 18 + order_id: 5 + operator_profile_id: + activity_type: paid + note: + created_at: '2022-10-04 14:25:37.278540' + updated_at: '2022-10-04 14:25:37.278540' +order_activity_19: + id: 19 + order_id: 14 + operator_profile_id: + activity_type: paid + note: + created_at: '2022-10-04 14:32:27.965847' + updated_at: '2022-10-04 14:32:27.965847' +order_activity_20: + id: 20 + order_id: 14 + operator_profile_id: 2437 + activity_type: ready + note: "Lorem ipsum dolor sit amet
" + created_at: '2022-10-04 14:33:17.067080' + updated_at: '2022-10-04 14:33:17.067080' +order_activity_21: + id: 21 + order_id: 15 + operator_profile_id: + activity_type: paid + note: + created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> +order_activity_22: + id: 22 + order_id: 15 + operator_profile_id: 2437 + activity_type: ready + note: "Votre commande est prête, merci de venir la récupérer jeudi + après 14 h.
" + created_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> diff --git a/test/fixtures/order_items.yml b/test/fixtures/order_items.yml new file mode 100644 index 000000000..4ce80a210 --- /dev/null +++ b/test/fixtures/order_items.yml @@ -0,0 +1,130 @@ +order_item_1: + id: 1 + order_id: 1 + orderable_type: Product + orderable_id: 18 + amount: 4000 + quantity: 1 + is_offered: + created_at: '2022-09-20 15:14:05.605885' + updated_at: '2022-09-20 15:14:05.605885' +order_item_2: + id: 2 + order_id: 1 + orderable_type: Product + orderable_id: 13 + amount: 500 + quantity: 1 + is_offered: + created_at: '2022-09-20 15:14:09.022448' + updated_at: '2022-09-20 15:14:09.022448' +order_item_3: + id: 3 + order_id: 2 + orderable_type: Product + orderable_id: 15 + amount: 1500 + quantity: 4 + is_offered: + created_at: '2022-09-20 15:14:34.899658' + updated_at: '2022-09-20 15:14:34.899658' +order_item_11: + id: 11 + order_id: 5 + orderable_type: Product + orderable_id: 18 + amount: 4000 + quantity: 3 + is_offered: + created_at: '2022-09-28 08:31:30.377083' + updated_at: '2022-10-04 08:34:25.130457' +order_item_16: + id: 16 + order_id: 9 + orderable_type: Product + orderable_id: 23 + amount: 119 + quantity: 1 + is_offered: + created_at: '2022-10-04 12:35:39.730968' + updated_at: '2022-10-04 12:35:39.730968' +order_item_17: + id: 17 + order_id: 9 + orderable_type: Product + orderable_id: 22 + amount: 200 + quantity: 1 + is_offered: + created_at: '2022-10-04 12:35:40.562076' + updated_at: '2022-10-04 12:35:40.562076' +order_item_18: + id: 18 + order_id: 10 + orderable_type: Product + orderable_id: 23 + amount: 119 + quantity: 1 + is_offered: + created_at: '2022-10-04 13:54:19.042858' + updated_at: '2022-10-04 13:54:19.042858' +order_item_19: + id: 19 + order_id: 10 + orderable_type: Product + orderable_id: 15 + amount: 1500 + quantity: 1 + is_offered: + created_at: '2022-10-04 13:54:22.649153' + updated_at: '2022-10-04 13:54:22.649153' +order_item_20: + id: 20 + order_id: 11 + orderable_type: Product + orderable_id: 11 + amount: 1000 + quantity: 1 + is_offered: + created_at: '2022-10-04 14:03:59.721736' + updated_at: '2022-10-04 14:03:59.721736' +order_item_21: + id: 21 + order_id: 12 + orderable_type: Product + orderable_id: 20 + amount: 1 + quantity: 2 + is_offered: + created_at: '2022-10-04 14:05:23.583721' + updated_at: '2022-10-04 14:17:41.866526' +order_item_22: + id: 22 + order_id: 12 + orderable_type: Product + orderable_id: 18 + amount: 4000 + quantity: 1 + is_offered: + created_at: '2022-10-04 14:17:33.257776' + updated_at: '2022-10-04 14:17:33.257776' +order_item_23: + id: 23 + order_id: 14 + orderable_type: Product + orderable_id: 18 + amount: 4000 + quantity: 3 + is_offered: + created_at: '2022-10-04 14:31:51.106853' + updated_at: '2022-10-04 14:31:54.194063' +order_item_24: + id: 24 + order_id: 15 + orderable_type: Product + orderable_id: 17 + amount: 3000 + quantity: 1 + is_offered: + created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> diff --git a/test/fixtures/orders.yml b/test/fixtures/orders.yml new file mode 100644 index 000000000..2802e9dec --- /dev/null +++ b/test/fixtures/orders.yml @@ -0,0 +1,162 @@ +order_1: + id: 1 + statistic_profile_id: 3 + operator_profile_id: 1 + token: 3R9wtsPjyYMHKqy-I2V5Cg1661868752184 + reference: + state: paid + total: 4500 + created_at: '2022-08-30 14:12:32.213832' + updated_at: '2022-09-20 15:14:23.501369' + wallet_amount: + wallet_transaction_id: + payment_method: local + footprint: + environment: test + coupon_id: + paid_total: 4500 + invoice_id: 5811 +order_2: + id: 2 + statistic_profile_id: 7 + operator_profile_id: 1 + token: TDKq6p4f3WgDRMIvplEqpg1663686863871 + reference: '005877-09-22' + state: ready + total: 6000 + created_at: '2022-09-20 15:14:23.887098' + updated_at: '2022-10-04 13:37:49.935380' + wallet_amount: + wallet_transaction_id: + payment_method: local + footprint: + environment: test + coupon_id: + paid_total: 6000 + invoice_id: 5812 +order_5: + id: 5 + statistic_profile_id: 3 + operator_profile_id: 1 + token: UbTOaTePhajDJPPH42e3yw1664353619348 + reference: '005882-09-22' + state: paid + total: 12000 + created_at: '2022-09-28 08:26:59.368029' + updated_at: '2022-10-04 14:25:37.345897' + wallet_amount: + wallet_transaction_id: + payment_method: local + footprint: + environment: test + coupon_id: + paid_total: 12000 + invoice_id: 5820 +order_9: + id: 9 + statistic_profile_id: 4 + operator_profile_id: 4 + token: 8jOZwd2vkE86x26cg167iQ1664886886040 + reference: '005888-10-22' + state: ready + total: 319 + created_at: '2022-10-04 12:34:46.054976' + updated_at: '2022-10-04 13:50:30.611456' + wallet_amount: + wallet_transaction_id: + payment_method: card + footprint: + environment: test + coupon_id: + paid_total: 319 + invoice_id: 5816 +order_10: + id: 10 + statistic_profile_id: 4 + operator_profile_id: 4 + token: ryrsm4yj21DTJv1qIgMuGA1664886964105 + reference: '005890-10-22' + state: ready + total: 1619 + created_at: '2022-10-04 12:36:04.117389' + updated_at: '2022-10-04 13:54:56.792550' + wallet_amount: + wallet_transaction_id: + payment_method: card + footprint: + environment: test + coupon_id: 30 + paid_total: 1296 + invoice_id: 5817 +order_11: + id: 11 + statistic_profile_id: 4 + operator_profile_id: 4 + token: ogvv31XzLE0zFLc_c8k4Sw1664891683573 + reference: '005892-10-22' + state: ready + total: 1000 + created_at: '2022-10-04 13:54:43.617309' + updated_at: '2022-10-04 14:04:28.490690' + wallet_amount: + wallet_transaction_id: + payment_method: card + footprint: + environment: test + coupon_id: + paid_total: 1000 + invoice_id: 5818 +order_12: + id: 12 + statistic_profile_id: 4 + operator_profile_id: 4 + token: ttG9U892Bu0gbu8OnJkwTw1664892253183 + reference: '005894-10-22' + state: ready + total: 4002 + created_at: '2022-10-04 14:04:13.206078' + updated_at: '2022-10-04 14:22:54.562186' + wallet_amount: + wallet_transaction_id: + payment_method: card + footprint: + environment: test + coupon_id: + paid_total: 4002 + invoice_id: 5819 +order_14: + id: 14 + statistic_profile_id: 2 + operator_profile_id: 1 + token: QsDdf_8YHL6mxUSMno-deg1664893537749 + reference: '005898-10-22' + state: ready + total: 12000 + created_at: '2022-10-04 14:25:37.773536' + updated_at: '2022-10-04 14:33:17.068764' + wallet_amount: + wallet_transaction_id: + payment_method: local + footprint: + environment: test + coupon_id: + paid_total: 12000 + invoice_id: 5821 +order_15: + id: 15 + statistic_profile_id: 2 + operator_profile_id: 1 + token: JOLm4fAZjFSkmWzAX3KObw1664893948693 + reference: '005900-10-22' + state: ready + total: 3000 + created_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + wallet_amount: + wallet_transaction_id: + payment_method: local + footprint: + environment: test + coupon_id: + paid_total: 3000 + invoice_id: 5822 diff --git a/test/fixtures/product_categories.yml b/test/fixtures/product_categories.yml new file mode 100644 index 000000000..8f58007f6 --- /dev/null +++ b/test/fixtures/product_categories.yml @@ -0,0 +1,49 @@ +product_category_1: + id: 1 + name: Consommables + slug: consommables + parent_id: + position: 0 + created_at: '2022-08-23 07:55:35.573968' + updated_at: '2022-08-23 07:55:35.573968' +product_category_2: + id: 2 + name: Fournitures + slug: fournitures + parent_id: + position: 1 + created_at: '2022-08-23 07:55:49.380035' + updated_at: '2022-10-05 11:42:52.965226' +product_category_3: + id: 3 + name: Filament imprimante 3D + slug: filament-imprimante-3d + parent_id: 1 + position: 0 + created_at: '2022-08-23 07:56:05.254962' + updated_at: '2022-08-23 07:56:05.254962' +product_category_4: + id: 4 + name: Objets créés au fablab + slug: objets-crees-au-fablab + parent_id: + position: 2 + created_at: '2022-09-14 13:39:03.170717' + updated_at: '2022-10-05 11:42:52.965226' +product_category_5: + id: 5 + name: Bois + slug: bois + parent_id: 1 + position: 1 + created_at: '2022-09-14 14:57:58.136610' + updated_at: '2022-09-14 14:57:58.136610' +product_category_6: + id: 6 + name: Stickers + slug: stickers + parent_id: 4 + position: 0 + created_at: '2022-09-28 08:12:35.823487' + updated_at: '2022-09-28 08:12:35.823487' + diff --git a/test/fixtures/product_stock_movements.yml b/test/fixtures/product_stock_movements.yml new file mode 100644 index 000000000..2a3b7fc9a --- /dev/null +++ b/test/fixtures/product_stock_movements.yml @@ -0,0 +1,275 @@ +product_stock_movement_26: + id: 26 + product_id: 3 + quantity: 1 + reason: inward_stock + stock_type: internal + remaining_stock: 1 + date: '2022-09-13 12:52:52.656974' + created_at: '2022-09-13 12:52:52.669247' + updated_at: '2022-09-13 12:52:52.669247' + order_item_id: +product_stock_movement_33: + id: 33 + product_id: 11 + quantity: 10 + reason: inward_stock + stock_type: external + remaining_stock: 10 + date: '2022-09-14 13:41:16.204940' + created_at: '2022-09-14 13:41:16.212290' + updated_at: '2022-09-14 13:41:16.212290' + order_item_id: +product_stock_movement_34: + id: 34 + product_id: 12 + quantity: 1 + reason: returned + stock_type: internal + remaining_stock: 1 + date: '2022-09-14 13:42:53.702489' + created_at: '2022-09-14 13:42:53.706634' + updated_at: '2022-09-14 13:42:53.706634' + order_item_id: +product_stock_movement_35: + id: 35 + product_id: 13 + quantity: 1000 + reason: inward_stock + stock_type: external + remaining_stock: 1000 + date: '2022-09-14 13:47:18.120615' + created_at: '2022-09-14 13:47:18.131919' + updated_at: '2022-09-14 13:47:18.131919' + order_item_id: +product_stock_movement_36: + id: 36 + product_id: 14 + quantity: 800 + reason: inward_stock + stock_type: external + remaining_stock: 800 + date: '2022-09-14 13:48:05.025884' + created_at: '2022-09-14 13:48:05.035926' + updated_at: '2022-09-14 13:48:05.035926' + order_item_id: +product_stock_movement_37: + id: 37 + product_id: 15 + quantity: 600 + reason: inward_stock + stock_type: external + remaining_stock: 600 + date: '2022-09-14 13:49:08.203980' + created_at: '2022-09-14 13:49:08.240896' + updated_at: '2022-09-14 13:49:08.240896' + order_item_id: +product_stock_movement_38: + id: 38 + product_id: 16 + quantity: 100 + reason: inward_stock + stock_type: external + remaining_stock: 100 + date: '2022-09-14 13:50:12.581463' + created_at: '2022-09-14 13:50:12.591335' + updated_at: '2022-09-14 13:50:12.591335' + order_item_id: +product_stock_movement_39: + id: 39 + product_id: 17 + quantity: 100 + reason: inward_stock + stock_type: external + remaining_stock: 100 + date: '2022-09-14 13:51:12.627746' + created_at: '2022-09-14 13:51:12.636692' + updated_at: '2022-09-14 13:51:12.636692' + order_item_id: +product_stock_movement_40: + id: 40 + product_id: 18 + quantity: 20 + reason: inward_stock + stock_type: external + remaining_stock: 20 + date: '2022-09-14 13:52:08.655537' + created_at: '2022-09-14 13:52:08.672419' + updated_at: '2022-09-14 13:52:08.672419' + order_item_id: +product_stock_movement_41: + id: 41 + product_id: 18 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 19 + date: '2022-09-20 15:14:22.629666' + created_at: '2022-09-20 15:14:22.651877' + updated_at: '2022-09-20 15:14:22.651877' + order_item_id: 1 +product_stock_movement_42: + id: 42 + product_id: 13 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 999 + date: '2022-09-20 15:14:22.688402' + created_at: '2022-09-20 15:14:22.692461' + updated_at: '2022-09-20 15:14:22.692461' + order_item_id: 2 +product_stock_movement_43: + id: 43 + product_id: 15 + quantity: -4 + reason: sold + stock_type: external + remaining_stock: 596 + date: '2022-09-20 15:14:48.330513' + created_at: '2022-09-20 15:14:48.334360' + updated_at: '2022-09-20 15:14:48.334360' + order_item_id: 3 +product_stock_movement_56: + id: 56 + product_id: 22 + quantity: 1000 + reason: inward_stock + stock_type: external + remaining_stock: 1000 + date: '2022-10-03 14:00:49.950506' + created_at: '2022-10-03 14:00:50.107429' + updated_at: '2022-10-03 14:00:50.107429' + order_item_id: +product_stock_movement_57: + id: 57 + product_id: 22 + quantity: 1000 + reason: inward_stock + stock_type: internal + remaining_stock: 1000 + date: '2022-10-03 14:00:49.950569' + created_at: '2022-10-03 14:00:50.136314' + updated_at: '2022-10-03 14:00:50.136314' + order_item_id: +product_stock_movement_58: + id: 58 + product_id: 23 + quantity: 1000 + reason: inward_stock + stock_type: internal + remaining_stock: 1000 + date: '2022-10-03 14:02:00.518281' + created_at: '2022-10-03 14:02:00.524640' + updated_at: '2022-10-03 14:02:00.524640' + order_item_id: +product_stock_movement_59: + id: 59 + product_id: 23 + quantity: 1000 + reason: inward_stock + stock_type: external + remaining_stock: 1000 + date: '2022-10-03 14:02:00.518337' + created_at: '2022-10-03 14:02:00.526611' + updated_at: '2022-10-03 14:02:00.526611' + order_item_id: +product_stock_movement_61: + id: 61 + product_id: 23 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 999 + date: '2022-10-04 12:36:02.765304' + created_at: '2022-10-04 12:36:02.788575' + updated_at: '2022-10-04 12:36:02.788575' + order_item_id: 16 +product_stock_movement_62: + id: 62 + product_id: 22 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 999 + date: '2022-10-04 12:36:02.827107' + created_at: '2022-10-04 12:36:02.832197' + updated_at: '2022-10-04 12:36:02.832197' + order_item_id: 17 +product_stock_movement_63: + id: 63 + product_id: 23 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 998 + date: '2022-10-04 13:54:42.768616' + created_at: '2022-10-04 13:54:42.783563' + updated_at: '2022-10-04 13:54:42.783563' + order_item_id: 18 +product_stock_movement_64: + id: 64 + product_id: 15 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 595 + date: '2022-10-04 13:54:42.785294' + created_at: '2022-10-04 13:54:42.788610' + updated_at: '2022-10-04 13:54:42.788610' + order_item_id: 19 +product_stock_movement_65: + id: 65 + product_id: 11 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 9 + date: '2022-10-04 14:04:12.723429' + created_at: '2022-10-04 14:04:12.729897' + updated_at: '2022-10-04 14:04:12.729897' + order_item_id: 20 +product_stock_movement_67: + id: 67 + product_id: 18 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 18 + date: '2022-10-04 14:17:52.842942' + created_at: '2022-10-04 14:17:52.846168' + updated_at: '2022-10-04 14:17:52.846168' + order_item_id: 22 +product_stock_movement_68: + id: 68 + product_id: 18 + quantity: -3 + reason: sold + stock_type: external + remaining_stock: 15 + date: '2022-10-04 14:25:37.280032' + created_at: '2022-10-04 14:25:37.283491' + updated_at: '2022-10-04 14:25:37.283491' + order_item_id: 11 +product_stock_movement_69: + id: 69 + product_id: 18 + quantity: -3 + reason: sold + stock_type: external + remaining_stock: 12 + date: '2022-10-04 14:32:28.002830' + created_at: '2022-10-04 14:32:28.023296' + updated_at: '2022-10-04 14:32:28.023296' + order_item_id: 23 +product_stock_movement_70: + id: 70 + product_id: 17 + quantity: -1 + reason: sold + stock_type: external + remaining_stock: 99 + date: '2022-10-04 14:35:40.556113' + created_at: '2022-10-04 14:35:40.560251' + updated_at: '2022-10-04 14:35:40.560251' + order_item_id: 24 diff --git a/test/fixtures/products.yml b/test/fixtures/products.yml new file mode 100644 index 000000000..45da0d542 --- /dev/null +++ b/test/fixtures/products.yml @@ -0,0 +1,182 @@ +product_3: + id: 3 + name: Caisse en bois + slug: caisse-en-bois + sku: '1694813216840' + description: "C'est un superbe caisse en bois massif
" + is_active: true + product_category_id: 4 + amount: 52300 + quantity_min: 10 + stock: '{"external": 0, "internal": 1}' + low_stock_alert: true + low_stock_threshold: 10 + created_at: '2022-09-13 10:05:07.010322' + updated_at: '2022-09-14 13:39:55.309956' +product_11: + id: 11 + name: Bulldozer + slug: bulldozer + sku: '816516515' + description: "Jouet pour enfants
" + is_active: true + product_category_id: 4 + amount: 1000 + quantity_min: 1 + stock: '{"external": 9, "internal": 0}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-14 13:41:16.207154' + updated_at: '2022-10-04 14:04:12.727452' +product_12: + id: 12 + name: Casque à pointe + slug: casque-a-pointe + sku: '10210706' + description: "Authentique casque à pointe allemand de la 1ère guerre mondiale
" + is_active: true + product_category_id: 4 + amount: 142300 + quantity_min: 1 + stock: '{"external": 0, "internal": 1}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-14 13:42:36.729625' + updated_at: '2022-09-14 13:42:53.704455' +product_13: + id: 13 + name: Panneaux de MDF + slug: panneaux-de-mdf + sku: 3-8612 + description: '' + is_active: true + product_category_id: 5 + amount: 500 + quantity_min: 1 + stock: '{"external": 999, "internal": 0}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-14 13:47:18.123531' + updated_at: '2022-09-20 15:14:22.690742' +product_14: + id: 14 + name: Panneau de mélaminé + slug: panneau-de-melamine + sku: 12-4512 + description: '' + is_active: true + product_category_id: 5 + amount: 1000 + quantity_min: 1 + stock: '{"external": 800, "internal": 0}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-14 13:48:05.028266' + updated_at: '2022-09-14 14:58:28.577253' +product_15: + id: 15 + name: Panneau de contre-plaqué + slug: panneau-de-contre-plaque + sku: 12-4613 + description: '' + is_active: true + product_category_id: 5 + amount: 1500 + quantity_min: 1 + stock: '{"external": 595, "internal": 0}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-14 13:49:08.233064' + updated_at: '2022-10-04 13:54:42.787290' +product_16: + id: 16 + name: Panneau d'OSB + slug: panneau-osb + sku: 14-14488 + description: '' + is_active: true + product_category_id: 5 + amount: 1300 + quantity_min: 1 + stock: '{"external": 100, "internal": 0}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-14 13:50:12.584041' + updated_at: '2022-09-14 14:58:38.374112' +product_17: + id: 17 + name: Panneau de 3 plis mélèze + slug: panneau-de-3-plis-meleze + sku: 14-47887 + description: '' + is_active: true + product_category_id: 5 + amount: 3000 + quantity_min: 1 + stock: '{"external": 99, "internal": 0}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-14 13:51:12.630109' + updated_at: '2022-10-04 14:35:40.558485' +product_18: + id: 18 + name: Tablette lamellé-collé + slug: tablette-lamelle-colle + sku: 14-879895 + description: '' + is_active: true + product_category_id: 5 + amount: 4000 + quantity_min: 1 + stock: '{"external": 12, "internal": 0}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-14 13:52:08.659248' + updated_at: '2022-10-04 14:32:28.021046' +product_20: + id: 20 + name: Sticker Hello + slug: sticker-hello + sku: 63-44801 + description:un joli sticker en forme de smiley qui dit bonjour
+ is_active: true + product_category_id: 6 + amount: 1 + quantity_min: 1 + stock: '{"external": 15, "internal": 0}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-09-28 08:16:32.348880' + updated_at: '2022-10-04 14:17:52.839249' +product_22: + id: 22 + name: Filament PLA bleu + slug: filament-pla-bleu + sku: 984-777 + description:Filament + PLA 1,75 mm de couleur bleue
+ is_active: true + product_category_id: 3 + amount: 200 + quantity_min: 1 + stock: '{"external": 999, "internal": 1000}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-10-03 14:00:49.967342' + updated_at: '2022-10-04 12:36:02.830248' +product_23: + id: 23 + name: Filament PLA blanc + slug: filament-pla-blanc + sku: 178-774 + description: "Filament PLA 1,75 mm blanc
" + is_active: true + product_category_id: 3 + amount: 119 + quantity_min: 1 + stock: '{"external": 998, "internal": 1000}' + low_stock_alert: false + low_stock_threshold: + created_at: '2022-10-03 14:01:43.706366' + updated_at: '2022-10-04 13:54:42.782174' diff --git a/test/fixtures/users_roles.yml b/test/fixtures/users_roles.yml index 291b7625b..19f33c177 100644 --- a/test/fixtures/users_roles.yml +++ b/test/fixtures/users_roles.yml @@ -27,3 +27,18 @@ users_role_6: user_id: 6 role_id: 4 +users_role_7: + user_id: 7 + role_id: 2 + +users_role_8: + user_id: 8 + role_id: 2 + +users_role_9: + user_id: 9 + role_id: 2 + +users_role_10: + user_id: 10 + role_id: 2 diff --git a/test/services/statistic_service_test.rb b/test/services/statistics/reservation_subscription_statistic_service_test.rb similarity index 98% rename from test/services/statistic_service_test.rb rename to test/services/statistics/reservation_subscription_statistic_service_test.rb index e04412be6..ef9afb4e5 100644 --- a/test/services/statistic_service_test.rb +++ b/test/services/statistics/reservation_subscription_statistic_service_test.rb @@ -2,7 +2,7 @@ require 'test_helper' -class StatisticServiceTest < ActionDispatch::IntegrationTest +class ReservationSubscriptionStatisticServiceTest < ActionDispatch::IntegrationTest setup do @user = User.members.without_subscription.first @admin = User.with_role(:admin).first diff --git a/test/services/statistics/store_statistic_service_test.rb b/test/services/statistics/store_statistic_service_test.rb new file mode 100644 index 000000000..f94a74529 --- /dev/null +++ b/test/services/statistics/store_statistic_service_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'test_helper' + +class StoreStatisticServiceTest < ActionDispatch::IntegrationTest + setup do + @order = Order.find(15) + end + + test 'build stats about orders' do + # Build the stats for the last 3 days, we expect the above invoices (reservations+subscription) to appear in the resulting stats + ::Statistics::BuilderService.generate_statistic({ start_date: DateTime.current.beginning_of_day, + end_date: DateTime.current.end_of_day }) + + Stats::Order.refresh_index! + + # we should find order id 15 (created today) + stat_order = Stats::Order.search(query: { bool: { must: [{ term: { date: DateTime.current.to_date.iso8601 } }, + { term: { type: 'order' } }] } }).first + assert_not_nil stat_order + assert_equal @order.id, stat_order['orderId'] + check_statistics_on_user(stat_order) + end + + def check_statistics_on_user(stat) + assert_equal @order.statistic_profile.str_gender, stat['gender'] + assert_equal @order.statistic_profile.age.to_i, stat['age'] + assert_equal @order.statistic_profile.group.slug, stat['group'] + end +end diff --git a/test/services/wallet_service_test.rb b/test/services/wallet_service_test.rb index 35ecbcad3..5fe5ada5c 100644 --- a/test/services/wallet_service_test.rb +++ b/test/services/wallet_service_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class WalletServiceTest < ActiveSupport::TestCase @@ -54,9 +56,9 @@ class WalletServiceTest < ActiveSupport::TestCase test 'rollback debited amount if has an error when create wallet transaction' do service = WalletService.new(wallet: @vlonchamp_wallet) expected_amount = @vlonchamp_wallet.amount - transaction = service.debit(5) + transaction = service.debit('error') @vlonchamp_wallet.reload - assert_equal @vlonchamp_wallet.amount, expected_amount + assert_equal expected_amount, @vlonchamp_wallet.amount assert_not transaction end end