diff --git a/.rubocop.yml b/.rubocop.yml index 2eca3bfc2..c4973fc79 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,7 @@ Metrics/LineLength: Max: 140 Metrics/MethodLength: - Max: 35 + Max: 36 Metrics/CyclomaticComplexity: Max: 13 Metrics/PerceivedComplexity: diff --git a/app/controllers/api/local_payment_controller.rb b/app/controllers/api/local_payment_controller.rb index 900f35c3b..126510dd4 100644 --- a/app/controllers/api/local_payment_controller.rb +++ b/app/controllers/api/local_payment_controller.rb @@ -8,13 +8,7 @@ class API::LocalPaymentController < API::PaymentsController authorize LocalPaymentContext.new(cart, price[:amount]) - if cart.reservation - res = on_reservation_success(nil, nil, cart) - elsif cart.subscription - res = on_subscription_success(nil, nil, cart) - end - - render res + render on_payment_success(nil, nil, cart) end protected diff --git a/app/controllers/api/payments_controller.rb b/app/controllers/api/payments_controller.rb index ebbc5d9ed..06e49ed47 100644 --- a/app/controllers/api/payments_controller.rb +++ b/app/controllers/api/payments_controller.rb @@ -12,11 +12,7 @@ class API::PaymentsController < API::ApiController protected - def post_save(_gateway_item_id, _gateway_item_type); end - - def post_reservation_save(_gateway_item_id, _gateway_item_type); end - - def post_subscription_save(_gateway_item_id, _gateway_item_type); end + def post_save(_gateway_item_id, _gateway_item_type, _payment_document); end def get_wallet_debit(user, total_amount) wallet_amount = (user.wallet.amount * 100).to_i @@ -37,44 +33,13 @@ class API::PaymentsController < API::ApiController cs.from_hash(params[:cart_items]) end - # @param cart {ShoppingCart} - def check_coupon(cart) - return if cart.coupon.nil? - - cart.coupon.coupon - end - - # @param cart {ShoppingCart} - def check_plan(cart) - return unless cart.subscription - - plan = cart.subscription.plan - raise InvalidGroupError if plan.group_id != current_user.group_id - end - - def on_success(gateway_item_id, gateway_item_type, cart) - cart.pay_and_save(gateway_item_id, gateway_item_type) - end - - def on_reservation_success(gateway_item_id, gateway_item_type, cart) - is_reserve = on_success(gateway_item_id, gateway_item_type, cart) - post_reservation_save(gateway_item_id, gateway_item_type) - - if is_reserve - { template: 'api/reservations/show', status: :created, location: @reservation } + def on_payment_success(gateway_item_id, gateway_item_type, cart) + res = cart.build_and_save(gateway_item_id, gateway_item_type) + if res[:success] + post_save(gateway_item_id, gateway_item_type, res[:payment]) + res[:payment].render_resource.merge(status: :created) else - { json: @reservation.errors, status: :unprocessable_entity } - end - end - - def on_subscription_success(gateway_item_id, gateway_item_type, cart) - is_subscribe = on_success(gateway_item_id, gateway_item_type, cart) - post_subscription_save(gateway_item_id, gateway_item_type) - - if is_subscribe - { template: 'api/subscriptions/show', status: :created, location: @subscription } - else - { json: @subscription.errors, status: :unprocessable_entity } + { json: res[:errors], status: :unprocessable_entity } end end end diff --git a/app/controllers/api/payzen_controller.rb b/app/controllers/api/payzen_controller.rb index 04b7bce5e..7cc33fb44 100644 --- a/app/controllers/api/payzen_controller.rb +++ b/app/controllers/api/payzen_controller.rb @@ -50,25 +50,17 @@ class API::PayzenController < API::PaymentsController cart = shopping_cart if order['answer']['transactions'].first['status'] == 'PAID' - if cart.reservation - res = on_reservation_success(params[:order_id], cart) - elsif cart.subscription - res = on_subscription_success(params[:order_id], cart) - end + render on_payment_success(params[:order_id], cart) + else + render json: order['answer'], status: :unprocessable_entity end - - render res rescue StandardError => e render json: e, status: :unprocessable_entity end private - def on_reservation_success(order_id, cart) - super(order_id, 'PayZen::Order', cart) - end - - def on_subscription_success(order_id, cart) + def on_payment_success(order_id, cart) super(order_id, 'PayZen::Order', cart) end diff --git a/app/controllers/api/stripe_controller.rb b/app/controllers/api/stripe_controller.rb index 94701dc28..030bcabba 100644 --- a/app/controllers/api/stripe_controller.rb +++ b/app/controllers/api/stripe_controller.rb @@ -20,14 +20,11 @@ class API::StripeController < API::PaymentsController begin amount = debit_amount(cart) # will contains the amount and the details of each invoice lines if params[:payment_method_id].present? - check_coupon(cart) - check_plan(cart) - # Create the PaymentIntent intent = Stripe::PaymentIntent.create( { payment_method: params[:payment_method_id], - amount: Stripe::Service.stripe_amount(amount[:amount]), + amount: Stripe::Service.new.stripe_amount(amount[:amount]), currency: Setting.get('stripe_currency'), confirmation_method: 'manual', confirm: true, @@ -46,13 +43,7 @@ class API::StripeController < API::PaymentsController res = { json: { plan_id: 'this plan is not compatible with your current group' }, status: :unprocessable_entity } end - if intent&.status == 'succeeded' - if cart.reservation - res = on_reservation_success(intent, cart) - elsif cart.subscription - res = on_subscription_success(intent, cart) - end - end + res = on_payment_success(intent, cart) if intent&.status == 'succeeded' render generate_payment_response(intent, res) end @@ -82,14 +73,9 @@ class API::StripeController < API::PaymentsController cart = shopping_cart if intent&.status == 'succeeded' - if cart.reservation - res = on_reservation_success(intent, cart) - elsif cart.subscription - res = on_subscription_success(intent, cart) - end + res = on_payment_success(intent, cart) + render generate_payment_response(intent, res) end - - render generate_payment_response(intent, res) rescue Stripe::InvalidRequestError => e render json: e, status: :unprocessable_entity end @@ -107,31 +93,17 @@ class API::StripeController < API::PaymentsController private - def post_reservation_save(intent_id, intent_type) + def post_save(intent_id, intent_type, payment_document) return unless intent_type == 'Stripe::PaymentIntent' Stripe::PaymentIntent.update( intent_id, - { description: "Invoice reference: #{@reservation.invoice.reference}" }, + { description: "#{payment_document.class.name} reference: #{payment_document.reference}" }, { api_key: Setting.get('stripe_secret_key') } ) end - def post_subscription_save(intent_id, intent_type) - return unless intent_type == 'Stripe::PaymentIntent' - - Stripe::PaymentIntent.update( - intent_id, - { description: "Invoice reference: #{@subscription.invoices.first.reference}" }, - { api_key: Setting.get('stripe_secret_key') } - ) - end - - def on_reservation_success(intent, cart) - super(intent.id, intent.class.name, cart) - end - - def on_subscription_success(intent, cart) + def on_payment_success(intent, cart) super(intent.id, intent.class.name, cart) end diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index 1a0572ed5..713566bcc 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -613,12 +613,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', const updateCartPrice = function () { if (Object.keys($scope.user).length > 0) { const items = []; - if ($scope.selectedPlan) { - items.push(mkSubscription($scope.selectedPlan.id)); - } if ($scope.events.reserved && $scope.events.reserved.length > 0) { items.push(mkReservation($scope.events.reserved)); } + if ($scope.selectedPlan) { + items.push(mkSubscription($scope.selectedPlan.id)); + } return Price.compute(mkCartItems(items), function (res) { $scope.amountTotal = res.price; @@ -923,12 +923,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', */ const paySlots = function () { const items = []; - if ($scope.selectedPlan) { - items.push(mkSubscription($scope.selectedPlan.id)); - } if ($scope.events.reserved && $scope.events.reserved.length > 0) { items.push(mkReservation($scope.events.reserved)); } + if ($scope.selectedPlan) { + items.push(mkSubscription($scope.selectedPlan.id)); + } return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) { const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount); diff --git a/app/frontend/src/javascript/models/payment.ts b/app/frontend/src/javascript/models/payment.ts index 6352c9ab6..6d5c53635 100644 --- a/app/frontend/src/javascript/models/payment.ts +++ b/app/frontend/src/javascript/models/payment.ts @@ -23,6 +23,7 @@ export type CartItem = { reservation: Reservation }|{ subscription: Subscription export interface ShoppingCart { customer_id: number, + // WARNING: items ordering matters! The first item in the array will be considered as the main item items: Array, coupon_code?: string, payment_schedule?: boolean, diff --git a/app/models/cart_item/subscription.rb b/app/models/cart_item/subscription.rb index 286e33087..9e412adc3 100644 --- a/app/models/cart_item/subscription.rb +++ b/app/models/cart_item/subscription.rb @@ -2,8 +2,6 @@ # A subscription added to the shopping cart class CartItem::Subscription < CartItem::BaseItem - attr_reader :plan - def initialize(plan, customer) raise TypeError unless plan.is_a? Plan @@ -11,8 +9,14 @@ class CartItem::Subscription < CartItem::BaseItem @customer = customer end + def plan + raise InvalidGroupError if @plan.group_id != @customer.group_id + + @plan + end + def price - amount = @plan.amount + amount = plan.amount elements = { plan: amount } { elements: elements, amount: amount } diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 4900808b0..6db1212f3 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -165,6 +165,10 @@ class Invoice < PaymentDocument !payment_gateway_object.nil? && payment_method == 'card' end + def render_resource + { partial: 'api/invoices/invoice', locals: { invoice: self } } + end + private def generate_and_send_invoice @@ -186,5 +190,4 @@ class Invoice < PaymentDocument puts changes puts '---------------------------------' end - end diff --git a/app/models/invoice_item.rb b/app/models/invoice_item.rb index b0fbd9df5..3b9fc0519 100644 --- a/app/models/invoice_item.rb +++ b/app/models/invoice_item.rb @@ -3,7 +3,6 @@ # A single line inside an invoice. Can be a subscription or a reservation class InvoiceItem < Footprintable belongs_to :invoice - belongs_to :subscription has_one :invoice_item # associates invoice_items of an invoice to invoice_items of an Avoir has_one :payment_gateway_object, as: :item diff --git a/app/models/offer_day.rb b/app/models/offer_day.rb index 658447c31..36d754842 100644 --- a/app/models/offer_day.rb +++ b/app/models/offer_day.rb @@ -6,4 +6,13 @@ class OfferDay < ApplicationRecord has_many :invoice_items, as: :object, dependent: :destroy belongs_to :subscription + + # buying invoice + def original_invoice + invoice_items.select(:invoice_id) + .group(:invoice_id) + .map(&:invoice_id) + .map { |id| Invoice.find_by(id: id, type: nil) } + .first + end end diff --git a/app/models/payment_document.rb b/app/models/payment_document.rb index 71dd32630..39149a5c7 100644 --- a/app/models/payment_document.rb +++ b/app/models/payment_document.rb @@ -23,4 +23,6 @@ class PaymentDocument < Footprintable end def post_save(arg); end + + def render_resource; end end diff --git a/app/models/payment_schedule.rb b/app/models/payment_schedule.rb index cdddb27f6..2f747bb16 100644 --- a/app/models/payment_schedule.rb +++ b/app/models/payment_schedule.rb @@ -77,6 +77,10 @@ class PaymentSchedule < PaymentDocument PaymentGatewayService.new.create_subscription(self, gateway_method_id) end + def render_resource + { partial: 'api/payment_schedules/payment_schedule', locals: { payment_schedule: self } } + end + private def generate_and_send_document diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 8f6fd004e..6a326ca31 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -67,6 +67,18 @@ class Reservation < ApplicationRecord reservable.save! end + def original_payment_schedule + payment_schedule_object&.payment_schedule + end + + def original_invoice + invoice_items.select(:invoice_id) + .group(:invoice_id) + .map(&:invoice_id) + .map { |id| Invoice.find_by(id: id, type: nil) } + .first + end + private def machine_not_already_reserved diff --git a/app/models/shopping_cart.rb b/app/models/shopping_cart.rb index fe0b84046..44dcbfb88 100644 --- a/app/models/shopping_cart.rb +++ b/app/models/shopping_cart.rb @@ -20,14 +20,6 @@ class ShoppingCart @payment_schedule = payment_schedule end - def subscription - @items.find { |item| item.is_a? CartItem::Subscription } - end - - def reservation - @items.find { |item| item.is_a? CartItem::Reservation } - end - # compute the price details of the current shopping cart def total total_amount = 0 @@ -53,26 +45,26 @@ class ShoppingCart } end - def pay_and_save(payment_id, payment_type) + def build_and_save(payment_id, payment_type) price = total objects = [] + payment = nil ActiveRecord::Base.transaction do items.each do |item| object = item.to_object object.save objects.push(object) - raise ActiveRecord::Rollback unless object.errors.count.zero? + raise ActiveRecord::Rollback unless object.errors.empty? end payment = if price[:schedule] PaymentScheduleService.new.create( - subscription&.to_object, + objects, price[:before_coupon], - coupon: @coupon, + coupon: @coupon.coupon, operator: @operator, payment_method: @payment_method, user: @customer, - reservation: reservation&.to_object, payment_id: payment_id, payment_type: payment_type ) @@ -80,7 +72,8 @@ class ShoppingCart InvoicesService.create( price, @operator.invoicing_profile.id, - reservation: reservation&.to_object, + objects, + @customer, payment_id: payment_id, payment_type: payment_type, payment_method: @payment_method @@ -90,6 +83,6 @@ class ShoppingCart payment.post_save(payment_id) end - objects.map(&:errors).flatten.count.zero? + { success: objects.map(&:errors).flatten.map(&:empty?).all?, payment: payment, errors: objects.map(&:errors).flatten } end end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 475fedc94..e5c44d4e8 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -58,7 +58,7 @@ class Subscription < ApplicationRecord operator_profile_id: operator_profile_id, total: 0 ) - invoice.invoice_items.push InvoiceItem.new(amount: 0, description: plan.name, subscription_id: id, object: od) + invoice.invoice_items.push InvoiceItem.new(amount: 0, description: plan.name, object: od) invoice.save if save @@ -73,11 +73,16 @@ class Subscription < ApplicationRecord end def original_payment_schedule - # if the payment schedule was associated with this subscription, return it directly - return payment_schedule if payment_schedule + payment_schedule_object&.payment_schedule + end - # if it was associated with a reservation, query payment schedule from one of its items - PaymentScheduleItem.where("cast(details->>'subscription_id' AS int) = ?", id).first&.payment_schedule + # buying invoice + def original_invoice + invoice_items.select(:invoice_id) + .group(:invoice_id) + .map(&:invoice_id) + .map { |id| Invoice.find_by(id: id, type: nil) } + .first end private diff --git a/app/models/wallet_transaction.rb b/app/models/wallet_transaction.rb index 2677b9515..87311ce6b 100644 --- a/app/models/wallet_transaction.rb +++ b/app/models/wallet_transaction.rb @@ -19,4 +19,8 @@ class WalletTransaction < ApplicationRecord def user invoicing_profile.user end + + def original_invoice + invoice_item.invoice + end end diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index 9f9bfe109..e87f7cc34 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -97,9 +97,9 @@ class PDF::Invoice < Prawn::Document DATE: I18n.l(invoice.main_item.object.slots[0].start_at.to_date), TIME: I18n.l(invoice.main_item.object.slots[0].start_at, format: :hour_minute)) invoice.invoice_items.each do |item| - next unless item.subscription_id + next unless item.subscription - subscription = Subscription.find item.subscription_id + subscription = item.subscription cancellation = invoice.is_a?(Avoir) ? I18n.t('invoices.cancellation') + ' - ' : '' object = "\n- #{object}\n- #{cancellation + subscription_verbose(subscription, name)}" break @@ -132,8 +132,8 @@ class PDF::Invoice < Prawn::Document details = invoice.is_a?(Avoir) ? I18n.t('invoices.cancellation') + ' - ' : '' - if item.subscription_id ### Subscription - subscription = Subscription.find item.subscription_id + if item.subscription ### Subscription + subscription = item.subscription if invoice.main_item.object_type == 'OfferDay' details += I18n.t('invoices.subscription_extended_for_free_from_START_to_END', START: I18n.l(invoice.main_item.object.start_at.to_date), diff --git a/app/services/invoices_service.rb b/app/services/invoices_service.rb index f6258a9b0..ae0f3ade1 100644 --- a/app/services/invoices_service.rb +++ b/app/services/invoices_service.rb @@ -64,13 +64,12 @@ class InvoicesService # Create an Invoice with an associated array of InvoiceItem matching the given parameters # @param payment_details {Hash} as generated by ShoppingCart.total # @param operator_profile_id {Number} ID of the user that operates the invoice generation (may be an admin, a manager or the customer himself) - # @param reservation {Reservation} the booking reservation, if any - # @param subscription {Subscription} the booking subscription, if any + # @param objects {Array} the booking reservation and/or subscription + # @param user {User} the customer # @param payment_id {String} ID of the payment, a returned by the gateway, if the current invoice is paid by card # @param payment_method {String} the payment method used ## - def self.create(payment_details, operator_profile_id, reservation: nil, subscription: nil, payment_id: nil, payment_type: nil, payment_method: nil) - user = reservation&.user || subscription&.user + def self.create(payment_details, operator_profile_id, objects, user, payment_id: nil, payment_type: nil, payment_method: nil) operator = InvoicingProfile.find(operator_profile_id)&.user method = if payment_method payment_method @@ -88,7 +87,7 @@ class InvoicesService invoice.payment_gateway_object = PaymentGatewayObject.new(gateway_object_id: payment_id, gateway_object_type: payment_type) end - InvoicesService.generate_invoice_items(invoice, payment_details, reservation: reservation, subscription: subscription) + InvoicesService.generate_invoice_items(invoice, payment_details, objects) InvoicesService.set_total_and_coupon(invoice, user, payment_details[:coupon]) invoice end @@ -97,30 +96,27 @@ class InvoicesService # Generate an array of {InvoiceItem} with the elements in provided reservation, price included. # @param invoice {Invoice} the parent invoice # @param payment_details {Hash} as generated by ShoppingCart.total + # @param objects {Array} ## - def self.generate_invoice_items(invoice, payment_details, reservation: nil, subscription: nil) - if reservation - case reservation.reservable - # === Event reservation === - when Event - InvoicesService.generate_event_item(invoice, reservation, payment_details) - # === Space|Machine|Training reservation === + def self.generate_invoice_items(invoice, payment_details, objects) + objects.each_with_index do |object, index| + if object.is_a?(Reservation) && object.reservable.is_a?(Event) + InvoicesService.generate_event_item(invoice, object, payment_details, index.zero?) + elsif object.is_a?(Subscription) + InvoicesService.generate_subscription_item(invoice, object, payment_details, index.zero?) + elsif object.is_a?(Reservation) + InvoicesService.generate_reservation_item(invoice, object, payment_details, index.zero?) else - InvoicesService.generate_generic_item(invoice, reservation, payment_details) + InvoicesService.generate_generic_item(invoice, object, payment_details, index.zero?) end end - - return unless subscription || reservation&.plan_id - - subscription = reservation.generate_subscription if !subscription && reservation.plan_id - InvoicesService.generate_subscription_item(invoice, subscription, payment_details, reservation.nil?) end ## # Generate an InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items. # This method must be called if reservation.reservable is an Event ## - def self.generate_event_item(invoice, reservation, payment_details) + def self.generate_event_item(invoice, reservation, payment_details, main = false) raise TypeError unless reservation.reservable.is_a? Event reservation.slots.each do |slot| @@ -142,7 +138,7 @@ class InvoicesService amount: price_slot[:price], description: description, object: reservation, - main: true + main: main ) end end @@ -151,7 +147,7 @@ class InvoicesService # Generate an InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items. # This method must be called if reservation.reservable is a Space, a Machine or a Training ## - def self.generate_generic_item(invoice, reservation, payment_details) + def self.generate_reservation_item(invoice, reservation, payment_details, main = false) raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class reservation.slots.each do |slot| @@ -163,7 +159,7 @@ class InvoicesService amount: price_slot[:price], description: description, object: reservation, - main: true + main: main ) end end @@ -172,18 +168,26 @@ class InvoicesService # Generate an InvoiceItem for the given subscription and save it in invoice.invoice_items. # This method must be called only with a valid subscription ## - def self.generate_subscription_item(invoice, subscription, payment_details, main = true) + def self.generate_subscription_item(invoice, subscription, payment_details, main = false) raise TypeError unless subscription invoice.invoice_items.push InvoiceItem.new( amount: payment_details[:elements][:plan], description: subscription.plan.name, - subscription_id: subscription.id, object: subscription, main: main ) end + def self.generate_generic_item(invoice, item, payment_details, main = false) + invoice.invoice_items.push InvoiceItem.new( + amount: payment_details[:elements][item.class.name.to_sym], + description: item.class.name, + object: item, + main: main + ) + end + ## # Set the total price to the reservation's invoice, summing its whole items. diff --git a/app/services/payment_schedule_service.rb b/app/services/payment_schedule_service.rb index 132064ded..a1f56bdb1 100644 --- a/app/services/payment_schedule_service.rb +++ b/app/services/payment_schedule_service.rb @@ -8,7 +8,7 @@ class PaymentScheduleService # @param total {Number} Total amount of the current shopping cart (which includes this plan) - without coupon # @param coupon {Coupon} apply this coupon, if any ## - def compute(plan, total, coupon: nil, subscription: nil) + def compute(plan, total, coupon: nil) other_items = total - plan.amount # base monthly price of the plan price = plan.amount @@ -23,7 +23,7 @@ class PaymentScheduleService items = [] (0..deadlines - 1).each do |i| date = DateTime.current + i.months - details = { recurring: per_month, subscription_id: subscription&.id } + details = { recurring: per_month } amount = if i.zero? details[:adjustment] = adjustment.truncate details[:other_items] = other_items.truncate @@ -49,21 +49,15 @@ class PaymentScheduleService { payment_schedule: ps, items: items } end - def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil, + def create(objects, total, coupon: nil, operator: nil, payment_method: nil, user: nil, payment_id: nil, payment_type: nil) - subscription = reservation.generate_subscription if !subscription && reservation&.plan_id - raise InvalidSubscriptionError unless subscription&.persisted? + subscription = objects.find { |item| item.class == Subscription } - schedule = compute(subscription.plan, total, coupon: coupon, subscription: subscription) + schedule = compute(subscription.plan, total, coupon: coupon) ps = schedule[:payment_schedule] items = schedule[:items] - if reservation - ps.payment_schedule_objects.push(PaymentScheduleObject.new(object: reservation, main: true)) - ps.payment_schedule_objects.push(PaymentScheduleObject.new(object: subscription, main: false)) if subscription - else - ps.payment_schedule_objects.push(PaymentScheduleObject.new(object: subscription, main: true)) - end + ps.payment_schedule_objects = build_objects(objects) ps.payment_method = payment_method if !payment_id.nil? && !payment_type.nil? pgo = PaymentGatewayObject.new( @@ -77,12 +71,18 @@ class PaymentScheduleService ps.invoicing_profile = user.invoicing_profile ps.statistic_profile = user.statistic_profile ps.payment_schedule_items = items - items.each do |item| - item.payment_schedule = ps - end ps end + def build_objects(objects) + res = [] + res.push(PaymentScheduleObject.new(object: objects[0], main: true)) + objects[1..-1].each do |object| + res.push(PaymentScheduleObject.new(object: object)) + end + res + end + ## # Generate the invoice associated with the given PaymentScheduleItem, with the children elements (InvoiceItems). # @param payment_method {String} the payment method or gateway in use diff --git a/app/services/subscriptions/subscribe.rb b/app/services/subscriptions/subscribe.rb index 0f55eec93..1fd5e8ffa 100644 --- a/app/services/subscriptions/subscribe.rb +++ b/app/services/subscriptions/subscribe.rb @@ -37,7 +37,7 @@ class Subscriptions::Subscribe operator = InvoicingProfile.find(operator_profile_id)&.user PaymentScheduleService.new.create( - new_sub, + [new_sub], details[:before_coupon], operator: operator, payment_method: schedule.payment_method, @@ -49,7 +49,8 @@ class Subscriptions::Subscribe InvoicesService.create( details, operator_profile_id, - subscription: new_sub + [new_sub], + new_sub.user ) end payment.save diff --git a/app/services/users_export_service.rb b/app/services/users_export_service.rb index 7eec9fdd6..74406e401 100644 --- a/app/services/users_export_service.rb +++ b/app/services/users_export_service.rb @@ -31,7 +31,7 @@ class UsersExportService # export reservations def export_reservations(export) - @reservations = Reservation.all.includes(:slots, :reservable, :invoice, statistic_profile: [user: [:profile]]) + @reservations = Reservation.all.includes(:slots, :reservable, statistic_profile: [user: [:profile]]) ActionController::Base.prepend_view_path './app/views/' # place data in view_assigns diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index 2470f9666..595e955d5 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -69,6 +69,7 @@ class WalletService ii.description = I18n.t('invoices.wallet_credit') ii.object = wallet_transaction ii.invoice = avoir + ii.main = true ii.save! end diff --git a/app/views/api/invoices/_invoice.json.jbuilder b/app/views/api/invoices/_invoice.json.jbuilder new file mode 100644 index 000000000..441e5e831 --- /dev/null +++ b/app/views/api/invoices/_invoice.json.jbuilder @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +json.extract! invoice, :id, :created_at, :reference, :avoir_date, :description +json.user_id invoice.invoicing_profile&.user_id +json.total invoice.total / 100.00 +json.name invoice.user.profile.full_name +json.has_avoir invoice.refunded? +json.is_avoir invoice.is_a?(Avoir) +json.is_subscription_invoice invoice.subscription_invoice? +json.stripe invoice.paid_by_card? +json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at +json.chained_footprint invoice.check_footprint +json.main_object do + json.type invoice.main_item.object_type + json.id invoice.main_item.object_id +end +json.items invoice.invoice_items do |item| + json.id item.id + json.amount item.amount / 100.0 + json.description item.description + json.avoir_item_id item.invoice_item.id if item.invoice_item +end diff --git a/app/views/api/invoices/show.json.jbuilder b/app/views/api/invoices/show.json.jbuilder index 1b818a8f2..f7cd588c6 100644 --- a/app/views/api/invoices/show.json.jbuilder +++ b/app/views/api/invoices/show.json.jbuilder @@ -1,21 +1,3 @@ # frozen_string_literal: true -json.extract! @invoice, :id, :created_at, :reference, :avoir_date, :description -json.user_id @invoice.invoicing_profile&.user_id -json.total @invoice.total / 100.00 -json.name @invoice.user.profile.full_name -json.has_avoir @invoice.refunded? -json.is_avoir @invoice.is_a?(Avoir) -json.is_subscription_invoice @invoice.subscription_invoice? -json.stripe @invoice.paid_by_card? -json.date @invoice.is_a?(Avoir) ? @invoice.avoir_date : @invoice.created_at -json.chained_footprint @invoice.check_footprint -json.main_object do - json.type @invoice.main_item.object_type -end -json.items @invoice.invoice_items do |item| - json.id item.id - json.amount item.amount / 100.0 - json.description item.description - json.avoir_item_id item.invoice_item.id if item.invoice_item -end +json.partial! 'api/invoices/invoice', invoice: @invoice diff --git a/app/views/api/payment_schedules/_payment_schedule.json.jbuilder b/app/views/api/payment_schedules/_payment_schedule.json.jbuilder index 1480cdf0f..b42e2188b 100644 --- a/app/views/api/payment_schedules/_payment_schedule.json.jbuilder +++ b/app/views/api/payment_schedules/_payment_schedule.json.jbuilder @@ -13,6 +13,10 @@ if payment_schedule.operator_profile json.extract! payment_schedule.operator_profile, :first_name, :last_name end end +json.main_object do + json.type payment_schedule.main_object.object_type + json.id payment_schedule.main_object.object_id +end json.items payment_schedule.payment_schedule_items do |item| json.extract! item, :id, :due_date, :state, :invoice_id, :payment_method json.amount item.amount / 100.00 diff --git a/app/views/exports/users_reservations.xlsx.axlsx b/app/views/exports/users_reservations.xlsx.axlsx index d58ae2b46..9fa08a9b0 100644 --- a/app/views/exports/users_reservations.xlsx.axlsx +++ b/app/views/exports/users_reservations.xlsx.axlsx @@ -1,7 +1,9 @@ +# frozen_string_literal: true + wb = xlsx_package.workbook -header = wb.styles.add_style b: true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF' -date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format +header = wb.styles.add_style b: true, bg_color: Stylesheet.primary.upcase.gsub('#', 'FF'), fg_color: 'FFFFFFFF' +date = wb.styles.add_style format_code: Rails.application.secrets.excel_date_format wb.add_worksheet(name: t('export_reservations.reservations')) do |sheet| @@ -15,17 +17,17 @@ wb.add_worksheet(name: t('export_reservations.reservations')) do |sheet| # data rows @reservations.each do |resrv| data = [ - resrv.user&.id, - resrv.user&.profile&.full_name || t('export_reservations.deleted_user'), - resrv.user&.email, - resrv.created_at.to_date, - resrv.reservable_type, - (resrv.reservable.nil? ? '' : resrv.reservable.name), - (resrv.reservable_type == 'Event') ? resrv.total_booked_seats: resrv.slots.count, - (resrv.invoice&.paid_by_card?) ? t('export_reservations.online_payment') : t('export_reservations.local_payment') + resrv.user&.id, + resrv.user&.profile&.full_name || t('export_reservations.deleted_user'), + resrv.user&.email, + resrv.created_at.to_date, + resrv.reservable_type, + resrv.reservable.nil? ? '' : resrv.reservable.name, + resrv.reservable_type == 'Event' ? resrv.total_booked_seats : resrv.slots.count, + resrv.original_invoice&.paid_by_card? ? t('export_reservations.online_payment') : t('export_reservations.local_payment') ] styles = [nil, nil, nil, date, nil, nil, nil, nil] - types = [:integer, :string, :string, :date, :string, :string, :integer, :string] + types = %i[integer string string date string string integer string] sheet.add_row data, style: styles, types: types end diff --git a/db/structure.sql b/db/structure.sql index bdbc3237d..b19c1e543 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -5213,22 +5213,6 @@ CREATE INDEX profiles_lower_unaccent_last_name_trgm_idx ON public.profiles USING CREATE INDEX projects_search_vector_idx ON public.projects USING gin (search_vector); --- --- Name: accounting_periods accounting_periods_del_protect; Type: RULE; Schema: public; Owner: - --- - -CREATE RULE accounting_periods_del_protect AS - ON DELETE TO public.accounting_periods DO INSTEAD NOTHING; - - --- --- Name: accounting_periods accounting_periods_upd_protect; Type: RULE; Schema: public; Owner: - --- - -CREATE RULE accounting_periods_upd_protect AS - ON UPDATE TO public.accounting_periods DO INSTEAD NOTHING; - - -- -- Name: projects projects_search_content_trigger; Type: TRIGGER; Schema: public; Owner: - -- diff --git a/lib/stripe/service.rb b/lib/stripe/service.rb index 08553184a..34370ed39 100644 --- a/lib/stripe/service.rb +++ b/lib/stripe/service.rb @@ -17,7 +17,7 @@ class Stripe::Service < Payment::Service subscription = payment_schedule.payment_schedule_objects.find(&:subscription) reservable_stp_id = payment_schedule.main_object.object.reservable&.payment_gateway_object&.gateway_object_id when Subscription.name - subscription = payment_schedule.main_object + subscription = payment_schedule.main_object.object reservable_stp_id = nil else raise InvalidSubscriptionError diff --git a/test/integration/events/as_admin_test.rb b/test/integration/events/as_admin_test.rb index cb088a066..48c255d4d 100644 --- a/test/integration/events/as_admin_test.rb +++ b/test/integration/events/as_admin_test.rb @@ -194,19 +194,19 @@ module Events assert_equal Mime[:json], response.content_type # Check the reservation match the required event - reservation = json_response(response.body) - r = Reservation.find(reservation[:id]) + result = json_response(response.body) + i = Invoice.find(result[:id]) - assert_equal e.id, r.reservable_id - assert_equal 'Event', r.reservable_type + assert_equal e.id, i.main_item.object_id + assert_equal 'Event', i.main_item.object_type # Check the remaining places were updated successfully e = Event.where(id: event[:id]).first assert_equal 2, e.nb_free_places, 'Number of free places was not updated' # Check the resulting invoice generation and it has right price - assert_invoice_pdf r.invoice - assert_equal (4 * 20) + (4 * 16), r.invoice.total / 100.0 + assert_invoice_pdf i + assert_equal (4 * 20) + (4 * 16), i.total / 100.0 end end end diff --git a/test/integration/reservations/create_as_admin_test.rb b/test/integration/reservations/create_as_admin_test.rb index 86f502a9e..125837bb9 100644 --- a/test/integration/reservations/create_as_admin_test.rb +++ b/test/integration/reservations/create_as_admin_test.rb @@ -54,11 +54,11 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 1, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count # invoice assertions - invoice = reservation.invoice + invoice = reservation.original_invoice assert invoice.payment_gateway_object.blank? refute invoice.total.blank? @@ -115,11 +115,11 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 1, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count # invoice assertions - invoice = reservation.invoice + invoice = reservation.original_invoice assert invoice.payment_gateway_object.blank? refute invoice.total.blank? @@ -186,11 +186,11 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 2, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count # invoice assertions - invoice = reservation.invoice + invoice = reservation.original_invoice assert invoice.payment_gateway_object.blank? refute invoice.total.blank? @@ -260,11 +260,11 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 1, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count # invoice assertions - invoice = reservation.invoice + invoice = reservation.original_invoice assert invoice.payment_gateway_object.blank? refute invoice.total.blank? @@ -307,11 +307,6 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest post '/api/local_payment/confirm_payment', params: { customer_id: @vlonchamp.id, items: [ - { - subscription: { - plan_id: plan.id, - } - }, { reservation: { reservable_id: machine.id, @@ -324,6 +319,11 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest } ] } + }, + { + subscription: { + plan_id: plan.id + } } ] }.to_json, headers: default_headers @@ -344,11 +344,11 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 2, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count # invoice assertions - invoice = reservation.invoice + invoice = reservation.original_invoice assert invoice.payment_gateway_object.blank? refute invoice.total.blank? @@ -415,7 +415,7 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert_not_nil reservation.invoice + assert_not_nil reservation.original_invoice # notification assert_not_empty Notification.where(attached_object: reservation) @@ -450,7 +450,7 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest }, { subscription: { - plan_id: plan.id, + plan_id: plan.id } } ] @@ -475,8 +475,8 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.find(result[:id]) - assert reservation.invoice - assert_equal 2, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count # credits assertions assert_equal 1, @user_without_subscription.credits.count @@ -484,7 +484,7 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest assert_equal training.id, @user_without_subscription.credits.last.creditable_id # invoice assertions - invoice = reservation.invoice + invoice = reservation.original_invoice assert invoice.payment_gateway_object.blank? refute invoice.total.blank? @@ -567,16 +567,9 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest assert_not_nil @user_without_subscription.subscription, "user's subscription was not found" assert_equal plan.id, @user_without_subscription.subscribed_plan.id, "user's plan does not match" - # Check the answer - reservation_res = json_response(response.body) - assert_equal plan.id, reservation_res[:user][:subscribed_plan][:id], 'subscribed plan does not match' - - # reservation assertions - assert_equal reservation_res[:id], reservation.id - assert reservation.payment_schedule - assert_equal payment_schedule.main_object.object, reservation - # payment schedule assertions + assert reservation.original_payment_schedule + assert_equal payment_schedule.id, reservation.original_payment_schedule.id assert_not_nil payment_schedule.reference assert_equal 'check', payment_schedule.payment_method assert_empty payment_schedule.payment_gateway_objects @@ -587,5 +580,13 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest assert payment_schedule.check_footprint assert_equal @user_without_subscription.invoicing_profile.id, payment_schedule.invoicing_profile_id assert_equal @admin.invoicing_profile.id, payment_schedule.operator_profile_id + + # Check the answer + result = json_response(response.body) + assert_equal reservation.original_payment_schedule.id, result[:id], 'payment schedule id does not match' + + # reservation assertions + assert_equal result[:main_object][:id], reservation.id + assert_equal payment_schedule.main_object.object, reservation end end diff --git a/test/integration/reservations/create_test.rb b/test/integration/reservations/create_test.rb index 134eeafd5..178b7a987 100644 --- a/test/integration/reservations/create_test.rb +++ b/test/integration/reservations/create_test.rb @@ -61,8 +61,8 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 1, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count # invoice_items assertions invoice_item = InvoiceItem.last @@ -183,8 +183,8 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 1, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count # invoice_items invoice_item = InvoiceItem.last @@ -261,8 +261,8 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 2, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count # invoice_items assertions invoice_items = InvoiceItem.last(2) @@ -338,8 +338,8 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 1, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count # invoice_items invoice_item = InvoiceItem.last @@ -418,8 +418,8 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 1, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count # invoice_items assertions invoice_item = InvoiceItem.last @@ -507,8 +507,8 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice - assert_equal 2, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count # invoice assertions item = InvoiceItem.find_by(object: reservation) @@ -591,8 +591,8 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest # reservation assertions reservation = Reservation.last - assert reservation.invoice_items.count == 1 - assert_equal 2, reservation.invoice.invoice_items.count + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count # invoice assertions item = InvoiceItem.find_by(object: reservation) @@ -605,17 +605,17 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest # invoice_items assertions ## reservation - reservation_item = invoice.invoice_items.where(subscription_id: nil).first + reservation_item = invoice.invoice_items.find_by(object: reservation) assert_not_nil reservation_item assert_equal reservation_item.amount, machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: plan.id).amount assert reservation_item.check_footprint ## subscription - subscription_item = invoice.invoice_items.where.not(subscription_id: nil).first + subscription_item = invoice.invoice_items.find_by(object_type: Subscription.name) assert_not_nil subscription_item - subscription = Subscription.find(subscription_item.subscription_id) + subscription = subscription_item.subscription assert_equal subscription_item.amount, plan.amount assert_equal subscription.plan_id, plan.id @@ -832,11 +832,6 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest coupon_code: 'GIME3EUR', payment_schedule: true, items: [ - { - subscription: { - plan_id: plan.id - } - }, { reservation: { reservable_id: machine.id, @@ -849,6 +844,11 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest } ] } + }, + { + subscription: { + plan_id: plan.id + } } ] }