diff --git a/Procfile b/Procfile
index bbda6f44f..e68a73938 100644
--- a/Procfile
+++ b/Procfile
@@ -1,4 +1,4 @@
-#web: bundle exec rails server puma -p $PORT
+web: bundle exec rails server puma -p $PORT
worker: bundle exec sidekiq -C ./config/sidekiq.yml
wp-client: bin/webpack-dev-server
wp-server: SERVER_BUNDLE_ONLY=yes bin/webpack --watch
diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js
index a209e394a..6510b643e 100644
--- a/app/frontend/src/javascript/directives/cart.js
+++ b/app/frontend/src/javascript/directives/cart.js
@@ -938,7 +938,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount);
- if ((AuthService.isAuthorized(['member']) && amountToPay > 0) ||
+ if ((AuthService.isAuthorized(['member']) && (amountToPay > 0 || (amountToPay === 0 && hasOtherDeadlines()))) ||
(AuthService.isAuthorized('manager') && $scope.user.id === $rootScope.currentUser.id && amountToPay > 0)) {
return payByStripe(reservation);
} else {
diff --git a/app/frontend/src/stylesheets/modules/fab-modal.scss b/app/frontend/src/stylesheets/modules/fab-modal.scss
index 7eb775fd2..f81344e64 100644
--- a/app/frontend/src/stylesheets/modules/fab-modal.scss
+++ b/app/frontend/src/stylesheets/modules/fab-modal.scss
@@ -17,6 +17,8 @@
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
animation: 0.15s linear fadeIn;
+ overflow-x: hidden;
+ overflow-y: auto;
}
.fab-modal-sm { width: 340px; }
diff --git a/app/frontend/templates/admin/coupons/_form.html b/app/frontend/templates/admin/coupons/_form.html
index 9cc475d0a..f326788e2 100644
--- a/app/frontend/templates/admin/coupons/_form.html
+++ b/app/frontend/templates/admin/coupons/_form.html
@@ -79,6 +79,8 @@
{{ 'app.shared.coupon.validity_per_user_is_required' }}
+
diff --git a/app/frontend/templates/wallet/transactions.html b/app/frontend/templates/wallet/transactions.html
index e8c90359e..edd5b5151 100644
--- a/app/frontend/templates/wallet/transactions.html
+++ b/app/frontend/templates/wallet/transactions.html
@@ -17,6 +17,9 @@
{{::t.invoice.reference}}
+
+ {{::t.payment_schedule.reference}}
+
{{ ::t.user.full_name }} |
diff --git a/app/models/coupon.rb b/app/models/coupon.rb
index 4e0a7d13b..e8f0194ae 100644
--- a/app/models/coupon.rb
+++ b/app/models/coupon.rb
@@ -32,13 +32,17 @@ class Coupon < ApplicationRecord
}
def safe_destroy
- if invoices.size.zero?
+ if usages.zero?
destroy
else
false
end
end
+ def usages
+ invoices.count + payment_schedule.count
+ end
+
##
# Check the status of the current coupon. The coupon:
# - may have been disabled by an admin,
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index d9e69313b..dbbc715ca 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -38,6 +38,7 @@ class Subscription < ApplicationRecord
end
def cancel
+ # TODO, currently unused, refactor to use with PaymentSchedule
update_columns(canceled_at: DateTime.current)
end
diff --git a/app/views/api/coupons/_coupon.json.jbuilder b/app/views/api/coupons/_coupon.json.jbuilder
index d4dd4cd82..bc71c5284 100644
--- a/app/views/api/coupons/_coupon.json.jbuilder
+++ b/app/views/api/coupons/_coupon.json.jbuilder
@@ -1,4 +1,4 @@
json.extract! coupon, :id, :name, :code, :type, :percent_off, :valid_until, :validity_per_user, :max_usages, :active, :created_at
json.amount_off (coupon.amount_off / 100.00) unless coupon.amount_off.nil?
-json.usages coupon.invoices.count
-json.status coupon.status
\ No newline at end of file
+json.usages coupon.usages
+json.status coupon.status
diff --git a/app/views/api/wallet/transactions.json.jbuilder b/app/views/api/wallet/transactions.json.jbuilder
index eaa955dd2..ba61cf89f 100644
--- a/app/views/api/wallet/transactions.json.jbuilder
+++ b/app/views/api/wallet/transactions.json.jbuilder
@@ -10,4 +10,10 @@ json.array!(@wallet_transactions) do |t|
json.reference t.invoice.reference
end
end
+ if t.payment_schedule
+ json.payment_schedule do
+ json.id t.payment_schedule.id
+ json.reference t.payment_schedule.reference
+ end
+ end
end
diff --git a/app/workers/stripe_worker.rb b/app/workers/stripe_worker.rb
index 3df6c087b..de5d2da18 100644
--- a/app/workers/stripe_worker.rb
+++ b/app/workers/stripe_worker.rb
@@ -49,5 +49,19 @@ class StripeWorker
object.update_attributes(stp_product_id: product.id)
puts "Stripe product was created for the #{class_name} \##{id}"
end
+
+ rescue Stripe::InvalidRequestError
+ STDERR.puts "WARNING: saved stp_product_id (#{object.stp_product_id}) does not match on Stripe, recreating..."
+ product = Stripe::Product.create(
+ {
+ name: object.name,
+ metadata: {
+ id: object.id,
+ type: class_name
+ }
+ }, { api_key: Setting.get('stripe_secret_key') }
+ )
+ object.update_attributes(stp_product_id: product.id)
+ puts "Stripe product was created for the #{class_name} \##{id}"
end
end
diff --git a/config/application.rb b/config/application.rb
index ef31793ab..8f6eafc0c 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -42,11 +42,6 @@ module Fablab
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
- #
- # /!\ ALL locales SHOULD be configured accordingly with this locale. /!\
- #
- config.i18n.default_locale = Rails.application.secrets.rails_locale
- config.i18n.fallbacks = [Rails.application.secrets.app_locale, :en]
config.to_prepare do
Devise::Mailer.layout 'notifications_mailer'
diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb
index 6cab93747..719b8fc60 100644
--- a/config/initializers/locale.rb
+++ b/config/initializers/locale.rb
@@ -5,3 +5,10 @@ I18n.config.available_locales += %i[en en-AU-CA en-GB en-IE en-IN en-NZ en-US en
es-AR es-CL es-CO es-CR es-DO es-EC es-ES es-MX es-PA es-PE es-US es-VE pt pt-BR zu]
# we allow the Zulu locale (zu) as it is used for In-Context translation
# @see https://support.crowdin.com/in-context-localization/
+
+
+#
+# /!\ ALL locales SHOULD be configured accordingly with the default_locale. /!\
+#
+I18n.config.default_locale = Rails.application.secrets.rails_locale
+I18n.config.locale = Rails.application.secrets.rails_locale
diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml
index 788c2f3d4..2a50f2f24 100644
--- a/config/locales/app.admin.en.yml
+++ b/config/locales/app.admin.en.yml
@@ -346,7 +346,7 @@ en:
confirmation_required: "Confirmation required"
do_you_really_want_to_delete_this_coupon: "Do you really want to delete this coupon?"
coupon_was_successfully_deleted: "Coupon was successfully deleted."
- unable_to_delete_the_specified_coupon_already_in_use: "Unable to delete the specified coupon: it is already used with some invoices."
+ unable_to_delete_the_specified_coupon_already_in_use: "Unable to delete the specified coupon: it is already used with some invoices and/or some payment schedules."
unable_to_delete_the_specified_coupon_an_unexpected_error_occurred: "Unable to delete the specified coupon: an unexpected error occurred."
send_a_coupon: "Send a coupon"
coupon: "Coupon"
diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml
index 69d3ab126..dca537f62 100644
--- a/config/locales/app.admin.fr.yml
+++ b/config/locales/app.admin.fr.yml
@@ -346,7 +346,7 @@ fr:
confirmation_required: "Confirmation requise"
do_you_really_want_to_delete_this_coupon: "Êtes-vous sûr(e) de vouloir supprimer ce code promotionnel ?"
coupon_was_successfully_deleted: "Le code promotionnel a bien été supprimé."
- unable_to_delete_the_specified_coupon_already_in_use: "Impossible de supprimer le code promotionnel : il est utilisé dans des factures."
+ unable_to_delete_the_specified_coupon_already_in_use: "Impossible de supprimer le code promotionnel : il est utilisé dans des factures et/ou des échéanciers."
unable_to_delete_the_specified_coupon_an_unexpected_error_occurred: "Impossible de supprimer le code promotionnel : une erreur inattendue s'est produite."
send_a_coupon: "Envoyer un code promo"
coupon: "Code promo"
diff --git a/config/routes.rb b/config/routes.rb
index 050864006..3c9131d96 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -111,6 +111,10 @@ Rails.application.routes.draw do
get 'first', action: 'first', on: :collection
end
+ resources :payment_schedules, only: %i[index show] do
+ get 'download', on: :member
+ end
+
resources :i_calendar, only: %i[index create destroy] do
get 'events', on: :member
post 'sync', on: :member
diff --git a/lib/tasks/fablab/stripe.rake b/lib/tasks/fablab/stripe.rake
index 082d67d98..2c605eafe 100644
--- a/lib/tasks/fablab/stripe.rake
+++ b/lib/tasks/fablab/stripe.rake
@@ -4,15 +4,6 @@
namespace :fablab do
namespace :stripe do
- desc 'Cancel stripe subscriptions'
- task cancel_subscriptions: :environment do
- Subscription.where('expiration_date >= ?', DateTime.current.at_beginning_of_day).each do |s|
- puts "-> Start cancel subscription of #{s.user.email}"
- s.cancel
- puts '-> Done'
- end
- end
-
desc 'find any invoices with incoherent total between stripe and DB'
task :find_incoherent_invoices, [:start_date] => :environment do |_task, args|
puts 'DEPRECATION WARNING: Will not work for invoices created from version 4.1.0 and above'
diff --git a/test/fixtures/coupons.yml b/test/fixtures/coupons.yml
index f2d485c34..2bf8adab7 100644
--- a/test/fixtures/coupons.yml
+++ b/test/fixtures/coupons.yml
@@ -5,7 +5,7 @@ one:
code: XMAS10
percent_off: 10
valid_until: 2015-12-31 23:59:59
- max_usages: nil
+ max_usages:
active: true
validity_per_user: once
@@ -32,6 +32,6 @@ cash2:
code: GIME3EUR
amount_off: 300
valid_until: <%= 1.year.from_now.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
- max_usages: nil
+ max_usages:
active: true
validity_per_user: once
diff --git a/test/fixtures/prices.yml b/test/fixtures/prices.yml
index e8f08d9ff..0e7ac37cd 100644
--- a/test/fixtures/prices.yml
+++ b/test/fixtures/prices.yml
@@ -2,7 +2,7 @@
price_1:
id: 1
group_id: 1
- plan_id:
+ plan_id:
priceable_id: 1
priceable_type: Machine
amount: 2400
@@ -12,7 +12,7 @@ price_1:
price_2:
id: 2
group_id: 2
- plan_id:
+ plan_id:
priceable_id: 1
priceable_type: Machine
amount: 5300
@@ -22,7 +22,7 @@ price_2:
price_5:
id: 5
group_id: 1
- plan_id:
+ plan_id:
priceable_id: 2
priceable_type: Machine
amount: 4200
@@ -32,7 +32,7 @@ price_5:
price_6:
id: 6
group_id: 2
- plan_id:
+ plan_id:
priceable_id: 2
priceable_type: Machine
amount: 1100
@@ -42,7 +42,7 @@ price_6:
price_9:
id: 9
group_id: 1
- plan_id:
+ plan_id:
priceable_id: 3
priceable_type: Machine
amount: 4100
@@ -52,7 +52,7 @@ price_9:
price_10:
id: 10
group_id: 2
- plan_id:
+ plan_id:
priceable_id: 3
priceable_type: Machine
amount: 5300
@@ -62,7 +62,7 @@ price_10:
price_13:
id: 13
group_id: 1
- plan_id:
+ plan_id:
priceable_id: 4
priceable_type: Machine
amount: 900
@@ -72,7 +72,7 @@ price_13:
price_14:
id: 14
group_id: 2
- plan_id:
+ plan_id:
priceable_id: 4
priceable_type: Machine
amount: 5100
@@ -82,7 +82,7 @@ price_14:
price_17:
id: 17
group_id: 1
- plan_id:
+ plan_id:
priceable_id: 5
priceable_type: Machine
amount: 1600
@@ -92,7 +92,7 @@ price_17:
price_18:
id: 18
group_id: 2
- plan_id:
+ plan_id:
priceable_id: 5
priceable_type: Machine
amount: 2000
@@ -102,7 +102,7 @@ price_18:
price_21:
id: 21
group_id: 1
- plan_id:
+ plan_id:
priceable_id: 6
priceable_type: Machine
amount: 3200
@@ -112,7 +112,7 @@ price_21:
price_22:
id: 22
group_id: 2
- plan_id:
+ plan_id:
priceable_id: 6
priceable_type: Machine
amount: 3400
@@ -298,3 +298,63 @@ price_42:
amount: 1000
created_at: 2016-04-04 15:18:28.860220000 Z
updated_at: 2016-04-04 15:18:50.517702000 Z
+
+price_43:
+ id: 43
+ group_id: 1
+ plan_id: 4
+ priceable_id: 1
+ priceable_type: Machine
+ amount: 1000
+ created_at: 2016-04-04 15:18:28.836899000 Z
+ updated_at: 2016-04-04 15:18:50.507019000 Z
+
+price_44:
+ id: 44
+ group_id: 1
+ plan_id: 4
+ priceable_id: 2
+ priceable_type: Machine
+ amount: 1000
+ created_at: 2016-04-04 15:18:28.842674000 Z
+ updated_at: 2016-04-04 15:18:50.508799000 Z
+
+price_45:
+ id: 45
+ group_id: 1
+ plan_id: 4
+ priceable_id: 3
+ priceable_type: Machine
+ amount: 1500
+ created_at: 2016-04-04 15:18:28.847736000 Z
+ updated_at: 2016-04-04 15:18:50.510437000 Z
+
+price_46:
+ id: 46
+ group_id: 1
+ plan_id: 4
+ priceable_id: 4
+ priceable_type: Machine
+ amount: 1000
+ created_at: 2016-04-04 15:18:28.852783000 Z
+ updated_at: 2016-04-04 15:18:50.512239000 Z
+
+price_47:
+ id: 47
+ group_id: 1
+ plan_id: 4
+ priceable_id: 5
+ priceable_type: Machine
+ amount: 800
+ created_at: 2016-04-04 15:18:28.856602000 Z
+ updated_at: 2016-04-04 15:18:50.514062000 Z
+
+price_48:
+ id: 48
+ group_id: 1
+ plan_id: 4
+ priceable_id: 6
+ priceable_type: Machine
+ amount: 1000
+ created_at: 2016-04-04 15:18:28.860220000 Z
+ updated_at: 2016-04-04 15:18:50.517702000 Z
diff --git a/test/fixtures/wallet_transactions.yml b/test/fixtures/wallet_transactions.yml
index 1f6eba863..6e521df85 100644
--- a/test/fixtures/wallet_transactions.yml
+++ b/test/fixtures/wallet_transactions.yml
@@ -1,7 +1,13 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
transaction1:
- invoicing_profile_id: 5
+ invoicing_profile_id: 1
wallet: wallet_5
transaction_type: credit
amount: 1000
+
+transaction2:
+ invoicing_profile_id: 1
+ wallet: wallet_7
+ transaction_type: credit
+ amount: 25500
diff --git a/test/fixtures/wallets.yml b/test/fixtures/wallets.yml
index 84521bdbd..5889fa6fa 100644
--- a/test/fixtures/wallets.yml
+++ b/test/fixtures/wallets.yml
@@ -24,4 +24,4 @@ wallet_1:
wallet_7:
invoicing_profile_id: 7
- amount: 0
+ amount: 25500
diff --git a/test/integration/reservations/create_test.rb b/test/integration/reservations/create_test.rb
index 7226d87fd..3ef638bf2 100644
--- a/test/integration/reservations/create_test.rb
+++ b/test/integration/reservations/create_test.rb
@@ -742,4 +742,115 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest
reservation = json_response(response.body)
assert_equal plan.id, reservation[:user][:subscribed_plan][:id], 'subscribed plan does not match'
end
+
+ test 'user reserves a machine and renew a subscription with payment schedule and coupon and wallet' do
+ user = User.find_by(username: 'lseguin')
+ login_as(user, scope: :user)
+
+ reservations_count = Reservation.count
+ invoice_count = Invoice.count
+ invoice_items_count = InvoiceItem.count
+ subscriptions_count = Subscription.count
+ user_subscriptions_count = user.subscriptions.count
+ payment_schedule_count = PaymentSchedule.count
+ payment_schedule_items_count = PaymentScheduleItem.count
+ wallet_transactions_count = WalletTransaction.count
+
+ machine = Machine.find(1)
+ availability = machine.availabilities.last
+ plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Abonnement mensualisable')
+
+ VCR.use_cassette('reservations_machine_subscription_with_payment_schedule_coupon_wallet') do
+ get "/api/payments/setup_intent/#{user.id}"
+
+ # Check response format & status
+ assert_equal 200, response.status, response.body
+ assert_equal Mime[:json], response.content_type
+
+ # Check the response
+ setup_intent = json_response(response.body)
+ assert_not_nil setup_intent[:client_secret]
+ assert_not_nil setup_intent[:id]
+ assert_match /^#{setup_intent[:id]}_secret_/, setup_intent[:client_secret]
+
+ # Confirm the intent (normally, this is done on browser-side)
+ stripe_res = Stripe::SetupIntent.confirm(
+ setup_intent[:id],
+ { payment_method: stripe_payment_method },
+ { api_key: Setting.get('stripe_secret_key') }
+ )
+
+ # check the confirmation
+ assert_equal setup_intent[:id], stripe_res.id
+ assert_equal 'succeeded', stripe_res.status
+ assert_equal 'off_session', stripe_res.usage
+
+
+ post '/api/payments/confirm_payment_schedule',
+ params: {
+ setup_intent_id: setup_intent[:id],
+ cart_items: {
+ coupon_code: 'GIME3EUR',
+ reservation: {
+ plan_id: plan.id,
+ payment_schedule: true,
+ reservable_id: machine.id,
+ reservable_type: machine.class.name,
+ slots_attributes: [
+ {
+ start_at: availability.start_at.to_s(:iso8601),
+ end_at: (availability.start_at + 1.hour).to_s(:iso8601),
+ availability_id: availability.id
+ }
+ ]
+ }
+ }
+ }.to_json, headers: default_headers
+ end
+
+ # Check response format & status
+ assert_equal 201, response.status, response.body
+ assert_equal Mime[:json], response.content_type
+ assert_equal reservations_count + 1, Reservation.count, 'missing the reservation'
+ assert_equal invoice_count, Invoice.count, "an invoice was generated but it shouldn't"
+ assert_equal invoice_items_count, InvoiceItem.count, "some invoice items were generated but they shouldn't"
+ assert_equal 0, UsersCredit.count, "user's credits were not reset"
+ assert_equal subscriptions_count + 1, Subscription.count, 'missing the subscription'
+ assert_equal payment_schedule_count + 1, PaymentSchedule.count, 'missing the payment schedule'
+ assert_equal payment_schedule_items_count + 12, PaymentScheduleItem.count, 'missing some payment schedule items'
+ assert_equal wallet_transactions_count + 1, WalletTransaction.count, 'missing the wallet transaction'
+
+ # get the objects
+ reservation = Reservation.last
+ subscription = Subscription.last
+ payment_schedule = PaymentSchedule.last
+
+ # subscription assertions
+ assert_equal user_subscriptions_count + 1, user.subscriptions.count
+ assert_equal user, subscription.user
+ assert_not_nil user.subscribed_plan, "user's subscribed plan was not found"
+ assert_not_nil user.subscription, "user's subscription was not found"
+ assert_equal plan.id, user.subscribed_plan.id, "user's plan does not match"
+
+ # reservation assertions
+ assert reservation.payment_schedule
+ assert_equal payment_schedule.scheduled, reservation
+
+ # payment schedule assertions
+ assert_not_nil payment_schedule.reference
+ assert_equal 'stripe', payment_schedule.payment_method
+ assert_not_nil payment_schedule.stp_subscription_id
+ assert_not_nil payment_schedule.stp_setup_intent_id
+ assert_not_nil payment_schedule.wallet_transaction
+ assert_equal payment_schedule.ordered_items.first.amount, payment_schedule.wallet_amount
+ assert_equal Coupon.find_by(code: 'GIME3EUR').id, payment_schedule.coupon_id
+ assert_equal 'test', payment_schedule.environment
+ assert payment_schedule.check_footprint
+ assert_equal user.invoicing_profile.id, payment_schedule.invoicing_profile_id
+ assert_equal payment_schedule.invoicing_profile_id, payment_schedule.operator_profile_id
+
+ # Check the answer
+ reservation = json_response(response.body)
+ assert_equal plan.id, reservation[:user][:subscribed_plan][:id], 'subscribed plan does not match'
+ end
end
|