From 44b5f6bff599f39ee71f85e4dd8d0897ab5e5d5d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 7 Sep 2022 14:27:21 +0200 Subject: [PATCH 01/17] New translations app.admin.en.yml (French) --- config/locales/app.admin.fr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index f67cab2ff..4009232af 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -1134,7 +1134,7 @@ fr: data_mapping: "Correspondance des données" TYPE_expected: "{TYPE} attendu" types: - integer: "integer" + integer: "entier" string: "chaîne" text: "texte" date: "date" @@ -1152,7 +1152,7 @@ fr: openid_connect_form: issuer: "Émetteur" issuer_help: "URL racine du serveur d'autorisation." - discovery: "Discovery" + discovery: "Découverte" discovery_help: "La découverte automatique OpenID doit-elle être utilisée ? Ceci est recommandé si l'IDP fournit un point d'accès de découverte automatique." discovery_unavailable: "La découverte automatique n'est pas disponible pour l'émetteur configuré." discovery_enabled: "Activer la découverte" From 8238fc36617fc97d3643c50e8d036cb65d616ecd Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 7 Sep 2022 14:27:21 +0200 Subject: [PATCH 02/17] New translations app.admin.en.yml (Portuguese) --- config/locales/app.admin.pt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index 732d6621a..15a2bd7b1 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -521,7 +521,7 @@ pt: warning_invoices_disabled: "Aviso: As faturas não estão ativadas. Nenhuma fatura será gerada pelo Fab-manager. No entanto, você deve preencher corretamente as informações abaixo, especialmente o IVA." change_logo: "Mudar o logo" john_smith: "João da Silva" - john_smith_at_example_com: "jean.smith@example.com" + john_smith_at_example_com: "joao.smith@example.com" invoice_reference_: "Referencia de fatura:" code_: "Código:" code_disabled: "Código desabilitado" From e3b62df7d48407b2f7636121350d82569a6c8a39 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 12 Sep 2022 09:57:54 +0200 Subject: [PATCH 03/17] (bug) admin can't move/cancel event reservation --- CHANGELOG.md | 1 + app/frontend/src/javascript/controllers/events.js.erb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef3db6c9..60944d13d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog Fab-manager - Script to download translations from Crowdin +- Fix a bug: admin and managers can't cancel or move event reservations ## v5.4.17 2022 September 06 diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index c7957ca3f..d933bc438 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -483,6 +483,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' * @param reservation {Reservation} */ $scope.reservationCanModify = function (reservation) { + if (AuthService.isAuthorized(['admin', 'manager'])) return true; + const slotStart = moment(reservation.slots_reservations_attributes[0].slot_attributes.start_at); const now = moment(); @@ -498,6 +500,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' * @param reservation {Reservation} */ $scope.reservationCanCancel = function(reservation) { + if (AuthService.isAuthorized(['admin', 'manager'])) return true; + const slotStart = moment(reservation.slots_reservations_attributes[0].slot_attributes.start_at); const now = moment(); return $scope.enableBookingCancel && slotStart.diff(now, "hours") >= $scope.cancelBookingDelay; From 3bf08973fb878eb3f4a397dc002e2ee59c9fad89 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 12 Sep 2022 11:50:58 +0200 Subject: [PATCH 04/17] (bug) can't complete profile if phone has hyphens or spaces --- CHANGELOG.md | 1 + .../src/javascript/components/user/user-profile-form.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60944d13d..de165e726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Script to download translations from Crowdin - Fix a bug: admin and managers can't cancel or move event reservations +- Fix a bug: phone numbers with hyphens and spaces prevent profile completion when the data is provided by a SSO ## v5.4.17 2022 September 06 diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx index 1c449c0e5..f4aedbb08 100644 --- a/app/frontend/src/javascript/components/user/user-profile-form.tsx +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -59,7 +59,7 @@ export const UserProfileForm: React.FC = ({ action, size, const { t } = useTranslation('shared'); // regular expression to validate the input fields - const phoneRegex = /^((00|\+)\d{2,3})?\d{4,14}$/; + const phoneRegex = /^((00|\+)\d{2,3})?[\d -]{4,14}$/; const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/; const { handleSubmit, register, control, formState, setValue, reset } = useForm({ defaultValues: { ...user } }); From 36704be01d44bead70bda1cfd8b580e89685feab Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 12 Sep 2022 12:19:31 +0200 Subject: [PATCH 05/17] (bug) do not verify group change when no initial group --- CHANGELOG.md | 3 ++- app/services/members/members_service.rb | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de165e726..7c0a692b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ - Script to download translations from Crowdin - Fix a bug: admin and managers can't cancel or move event reservations -- Fix a bug: phone numbers with hyphens and spaces prevent profile completion when the data is provided by a SSO +- Fix a bug: phone numbers with hyphens and spaces prevent profile completion when the data is provided by an SSO +- Fix a bug: unable to complete profile from SSO when the account validation is enabled ## v5.4.17 2022 September 06 diff --git a/app/services/members/members_service.rb b/app/services/members/members_service.rb index 48ca5101e..c1009d214 100644 --- a/app/services/members/members_service.rb +++ b/app/services/members/members_service.rb @@ -9,19 +9,19 @@ class Members::MembersService end def update(params) - if params[:group_id] && @member.group_id != params[:group_id].to_i && !@member.subscribed_plan.nil? + if subscriber_group_change?(params) # here a group change is requested but unprocessable, handle the exception @member.errors.add(:group_id, I18n.t('members.unable_to_change_the_group_while_a_subscription_is_running')) return false end - if params[:group_id] && params[:group_id].to_i != Group.find_by(slug: 'admins').id && @member.admin? + if admin_group_change?(params) # an admin cannot change his group @member.errors.add(:group_id, I18n.t('members.admins_cant_change_group')) return false end - group_changed = params[:group_id] && @member.group_id != params[:group_id].to_i + group_changed = user_group_change?(params) ex_group = @member.group user_validation_required = Setting.get('user_validation_required') @@ -80,7 +80,7 @@ class Members::MembersService end def validate(is_valid) - is_updated = member.update(validated_at: is_valid ? Time.now : nil) + is_updated = member.update(validated_at: is_valid ? DateTime.current : nil) if is_updated if is_valid NotificationCenter.call type: 'notify_user_is_validated', @@ -133,4 +133,16 @@ class Members::MembersService params[:password] end end + + def subscriber_group_change?(params) + params[:group_id] && @member.group_id != params[:group_id].to_i && !@member.subscribed_plan.nil? + end + + def admin_group_change?(params) + params[:group_id] && params[:group_id].to_i != Group.find_by(slug: 'admins').id && @member.admin? + end + + def user_group_change?(params) + @member.group_id && params[:group_id] && @member.group_id != params[:group_id].to_i + end end From b74d59014de4b2312b3071bf6d564a48bbd3a4be Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 12 Sep 2022 13:02:21 +0200 Subject: [PATCH 06/17] Version 5.4.18 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0a692b3..3dce351e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +## v5.4.18 2022 September 12 + - Script to download translations from Crowdin - Fix a bug: admin and managers can't cancel or move event reservations - Fix a bug: phone numbers with hyphens and spaces prevent profile completion when the data is provided by an SSO diff --git a/package.json b/package.json index dc778a1f6..85c8c01f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.17", + "version": "5.4.18", "description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.", "keywords": [ "fablab", From d491384326fc08e8d332af92dbc0748c92c0fdc5 Mon Sep 17 00:00:00 2001 From: Peng Date: Tue, 13 Sep 2022 10:27:46 +0000 Subject: [PATCH 07/17] (bug) fix comput the wallet amount that dont apply coupon --- app/services/wallet_service.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index 73f2b31ee..9966aa11a 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -75,17 +75,16 @@ class WalletService ## # Compute the amount decreased from the user's wallet, if applicable - # @param payment {Invoice|PaymentSchedule} + # @param payment {Invoice|PaymentSchedule|Order} # @param user {User} the customer - # @param coupon {Coupon|String} Coupon object or code ## - def self.wallet_amount_debit(payment, user, coupon = nil) + def self.wallet_amount_debit(payment, user) total = if payment.is_a? PaymentSchedule payment.payment_schedule_items.first.amount else payment.total end - total = CouponService.new.apply(total, coupon, user.id) if coupon + total = CouponService.new.apply(total, payment.coupon, user.id) if payment.coupon wallet_amount = (user.wallet.amount * 100).to_i From 1d1e839e830be3d60982fd112fded6e564f99bd5 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Tue, 13 Sep 2022 14:08:16 +0200 Subject: [PATCH 08/17] Version 5.4.19 --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dce351e6..5dc222f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog Fab-manager +## v5.4.19 2022 September 13 +- Fix a bug: comput the wallet amount that can't apply coupon + ## v5.4.18 2022 September 12 - Script to download translations from Crowdin diff --git a/package.json b/package.json index 85c8c01f1..2270235c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.18", + "version": "5.4.19", "description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.", "keywords": [ "fablab", From c6f0b69b2e72dfc6f78e6e928759fa9f8ed571b0 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 21 Sep 2022 13:56:46 +0200 Subject: [PATCH 09/17] (doc) Updated the link to the user's manual --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index 2e49f744b..24b581207 100644 --- a/doc/README.md +++ b/doc/README.md @@ -14,7 +14,7 @@ ### User's manual The following guide describes what you can do and how to use Fab-manager. - - [Français](fr/guide_utilisation_fab_manager_v5.0.pdf) + - [Français](http://guide-fr.fab.mn/) ### System administrator The following guides are designed for the people that perform software maintenance. From 9856450aacaf10f0e38825e5c441cd770d16a684 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 14 Sep 2022 10:34:12 +0200 Subject: [PATCH 10/17] (bug) broken daily view of the public agenda When the daily view contains trainings or events, the agenda cannot be filled with the slots because of an 500 error --- CHANGELOG.md | 4 +++- app/views/api/availabilities/public.json.jbuilder | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc222f5a..df271d8b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Changelog Fab-manager +- Fix a bug: unable to show the daily view of the public agenda, if it contains trainings or events + ## v5.4.19 2022 September 13 -- Fix a bug: comput the wallet amount that can't apply coupon +- Fix a bug: computing the wallet amount to debit ignores the applied coupon ## v5.4.18 2022 September 12 diff --git a/app/views/api/availabilities/public.json.jbuilder b/app/views/api/availabilities/public.json.jbuilder index 7add5228c..9720b430d 100644 --- a/app/views/api/availabilities/public.json.jbuilder +++ b/app/views/api/availabilities/public.json.jbuilder @@ -49,10 +49,10 @@ json.array!(@availabilities) do |availability| json.borderColor space_slot_border_color(availability) when 'training' json.training_id availability.availability.trainings.first.id - json.borderColor trainings_events_border_color(availability) + json.borderColor trainings_events_border_color(availability.availability) when 'event' json.event_id availability.availability.event.id - json.borderColor trainings_events_border_color(availability) + json.borderColor trainings_events_border_color(availability.availability) else json.title 'Unknown slot' end From 929c70b5bec9f4289ff2c262b26c146e0dd274a5 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 26 Sep 2022 11:31:45 +0200 Subject: [PATCH 11/17] (bug) plan's categories descriptions are not shown Also fixes a bug about empty groups displayed in the subscription page --- CHANGELOG.md | 2 ++ .../src/javascript/components/plans/plans-list.tsx | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df271d8b4..d94ffc3eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog Fab-manager - Fix a bug: unable to show the daily view of the public agenda, if it contains trainings or events +- Fix a bug: plan's categories descriptions are not shown +- Fix a bug: groups without plans are shown but empty ## v5.4.19 2022 September 13 - Fix a bug: computing the wallet amount to debit ignores the applied coupon diff --git a/app/frontend/src/javascript/components/plans/plans-list.tsx b/app/frontend/src/javascript/components/plans/plans-list.tsx index 328b0cd6d..9354da44b 100644 --- a/app/frontend/src/javascript/components/plans/plans-list.tsx +++ b/app/frontend/src/javascript/components/plans/plans-list.tsx @@ -117,10 +117,10 @@ export const PlansList: React.FC = ({ onError, onPlanSelection, }; /** - * When called with a category ID, returns the name of the requested plan-category + * When called with a category ID, returns the requested plan-category */ - const categoryName = (categoryId: number): string => { - return planCategories.find(c => c.id === categoryId)?.name; + const findCategory = (categoryId: number): PlanCategory => { + return planCategories.find(c => c.id === categoryId); }; /** @@ -193,10 +193,13 @@ export const PlansList: React.FC = ({ onError, onPlanSelection, return (
{Array.from(plans).sort(compareCategories).map(([categoryId, plansByCategory]) => { + const category = findCategory(categoryId); const categoryPlans = plansByCategory.filter(filterPlan); + const isShown = !!categoryId && categoryPlans.length > 0; return (
- {!!categoryId && categoryPlans.length > 0 &&

{ categoryName(categoryId) }

} + {isShown &&

{ category.name }

} + {isShown &&

} {renderPlans(categoryPlans)}

); @@ -232,7 +235,7 @@ export const PlansList: React.FC = ({ onError, onPlanSelection, {plans && Array.from(filteredPlans()).map(([groupId, plansByGroup]) => { return (
-

{ groupName(groupId) }

+ {plansByGroup.size > 0 &&

{ groupName(groupId) }

} {plansByGroup && renderPlansByCategory(plansByGroup)}
); From 4d72151ab54a0be99b0a5c2a2920adb8cf2b52ac Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 26 Sep 2022 11:41:58 +0200 Subject: [PATCH 12/17] (bug) unable to display the payment schedules management interface --- CHANGELOG.md | 1 + .../components/payment-schedule/payment-schedules-table.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d94ffc3eb..8a67856ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Fix a bug: unable to show the daily view of the public agenda, if it contains trainings or events - Fix a bug: plan's categories descriptions are not shown - Fix a bug: groups without plans are shown but empty +- Fix a bug: unable to display the payment schedules management interface ## v5.4.19 2022 September 13 - Fix a bug: computing the wallet amount to debit ignores the applied coupon diff --git a/app/frontend/src/javascript/components/payment-schedule/payment-schedules-table.tsx b/app/frontend/src/javascript/components/payment-schedule/payment-schedules-table.tsx index b9fba4fbd..96ee200c0 100644 --- a/app/frontend/src/javascript/components/payment-schedule/payment-schedules-table.tsx +++ b/app/frontend/src/javascript/components/payment-schedule/payment-schedules-table.tsx @@ -208,12 +208,12 @@ const PaymentSchedulesTable: React.FC = ({ paymentSc ); case 'payzen': + case null: return (
{renderPaymentSchedulesTable()}
); - case null: default: console.error(`[PaymentSchedulesTable] Unimplemented gateway: ${gateway.value}`); return
; From c43d39219103be18699d226c156b08dacfb8b790 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 27 Sep 2022 17:21:43 +0200 Subject: [PATCH 13/17] (test) fix test after v5.4.19 changes --- .rubocop.yml | 2 +- test/integration/reservations/create_test.rb | 927 ------------------ .../reservations/pay_with_wallet_test.rb | 277 ++++++ .../reservations/reserve_machine_test.rb | 232 +++++ .../reservations/reserve_space_test.rb | 83 ++ .../reservations/reserve_training_test.rb | 211 ++++ .../reservations/with_subscription_test.rb | 249 +++++ 7 files changed, 1053 insertions(+), 928 deletions(-) delete mode 100644 test/integration/reservations/create_test.rb create mode 100644 test/integration/reservations/pay_with_wallet_test.rb create mode 100644 test/integration/reservations/reserve_machine_test.rb create mode 100644 test/integration/reservations/reserve_space_test.rb create mode 100644 test/integration/reservations/reserve_training_test.rb create mode 100644 test/integration/reservations/with_subscription_test.rb diff --git a/.rubocop.yml b/.rubocop.yml index f423c323f..c48eca5a3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -13,7 +13,7 @@ Metrics/PerceivedComplexity: Metrics/AbcSize: Max: 45 Metrics/ClassLength: - Max: 200 + Max: 210 Metrics/BlockLength: Max: 30 Exclude: diff --git a/test/integration/reservations/create_test.rb b/test/integration/reservations/create_test.rb deleted file mode 100644 index 336f8ed9f..000000000 --- a/test/integration/reservations/create_test.rb +++ /dev/null @@ -1,927 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -module Reservations; end - -class Reservations::CreateTest < ActionDispatch::IntegrationTest - setup do - @user_without_subscription = User.members.without_subscription.first - @user_with_subscription = User.members.with_subscription.second - end - - test 'user without subscription reserves a machine with success' do - login_as(@user_without_subscription, scope: :user) - - machine = Machine.find(6) - availability = machine.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - users_credit_count = UsersCredit.count - subscriptions_count = Subscription.count - - VCR.use_cassette('reservations_create_for_machine_without_subscription_success') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - items: [ - { - reservation: { - reservable_id: machine.id, - reservable_type: machine.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - } - ] - } - }.to_json, headers: default_headers - end - - # general assertions - assert_equal 201, response.status - assert_equal reservations_count + 1, Reservation.count - assert_equal invoice_count + 1, Invoice.count - assert_equal invoice_items_count + 1, InvoiceItem.count - assert_equal users_credit_count, UsersCredit.count - assert_equal subscriptions_count, Subscription.count - - # subscription assertions - assert_equal 0, @user_without_subscription.subscriptions.count - assert_nil @user_without_subscription.subscribed_plan - - # reservation assertions - reservation = Reservation.last - - assert reservation.original_invoice - assert_equal 1, reservation.original_invoice.invoice_items.count - - # invoice_items assertions - invoice_item = InvoiceItem.last - - assert_equal machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount - assert invoice_item.check_footprint - - # invoice assertions - item = InvoiceItem.find_by(object: reservation) - invoice = item.invoice - assert_invoice_pdf invoice - assert_not_nil invoice.debug_footprint - - refute invoice.payment_gateway_object.blank? - refute invoice.total.blank? - assert invoice.check_footprint - - # notification - assert_not_empty Notification.where(attached_object: reservation) - end - - test 'user without subscription reserves a machine with error' do - login_as(@user_without_subscription, scope: :user) - - machine = Machine.find(6) - availability = machine.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - notifications_count = Notification.count - - VCR.use_cassette('reservations_create_for_machine_without_subscription_error') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method(error: :card_declined), - cart_items: { - items: [ - { - reservation: { - reservable_id: machine.id, - reservable_type: machine.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - } - ] - } - }.to_json, headers: default_headers - end - - # Check response format & status - assert_equal 200, response.status, "API does not return the expected status. #{response.body}" - assert_equal Mime[:json], response.content_type - - # Check the error was handled - assert_match /Your card was declined/, response.body - - # Check the subscription wasn't taken - assert_equal reservations_count, Reservation.count - assert_equal invoice_count, Invoice.count - assert_equal invoice_items_count, InvoiceItem.count - assert_equal notifications_count, Notification.count - - # subscription assertions - assert_equal 0, @user_without_subscription.subscriptions.count - assert_nil @user_without_subscription.subscribed_plan - end - - test 'user without subscription reserves a training with success' do - login_as(@user_without_subscription, scope: :user) - - training = Training.first - availability = training.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - - VCR.use_cassette('reservations_create_for_training_without_subscription_success') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - items: [ - { - reservation: { - reservable_id: training.id, - reservable_type: training.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - } - ] - } - }.to_json, headers: default_headers - end - - # general assertions - assert_equal 201, response.status - assert_equal reservations_count + 1, Reservation.count - assert_equal invoice_count + 1, Invoice.count - assert_equal invoice_items_count + 1, InvoiceItem.count - - # subscription assertions - assert_equal 0, @user_without_subscription.subscriptions.count - assert_nil @user_without_subscription.subscribed_plan - - # reservation assertions - reservation = Reservation.last - - assert reservation.original_invoice - assert_equal 1, reservation.original_invoice.invoice_items.count - - # invoice_items - invoice_item = InvoiceItem.last - - assert_equal invoice_item.amount, training.amount_by_group(@user_without_subscription.group_id).amount - assert invoice_item.check_footprint - - # invoice assertions - item = InvoiceItem.find_by(object: reservation) - invoice = item.invoice - assert_invoice_pdf invoice - assert_not_nil invoice.debug_footprint - - refute invoice.payment_gateway_object.blank? - refute invoice.total.blank? - assert invoice.check_footprint - - # notification - assert_not_empty Notification.where(attached_object: reservation) - end - - test 'user with subscription reserves a machine with success' do - login_as(@user_with_subscription, scope: :user) - - plan = @user_with_subscription.subscribed_plan - machine = Machine.find(6) - availability = machine.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - users_credit_count = UsersCredit.count - - VCR.use_cassette('reservations_create_for_machine_with_subscription_success') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - items: [ - { - reservation: { - reservable_id: machine.id, - reservable_type: machine.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - }, - { - slot_id: availability.slots.last.id - } - ] - } - } - ] - } - }.to_json, headers: default_headers - end - - # general assertions - assert_equal 201, response.status - assert_equal reservations_count + 1, Reservation.count - assert_equal invoice_count + 1, Invoice.count - assert_equal invoice_items_count + 2, InvoiceItem.count - assert_equal users_credit_count + 1, UsersCredit.count - - # subscription assertions - assert_equal 1, @user_with_subscription.subscriptions.count - assert_not_nil @user_with_subscription.subscribed_plan - assert_equal plan.id, @user_with_subscription.subscribed_plan.id - - # reservation assertions - reservation = Reservation.last - - assert reservation.original_invoice - assert_equal 2, reservation.original_invoice.invoice_items.count - - # invoice_items assertions - invoice_items = InvoiceItem.last(2) - machine_price = machine.prices.find_by(group_id: @user_with_subscription.group_id, plan_id: plan.id).amount - - assert(invoice_items.any? { |inv| inv.amount.zero? }) - assert(invoice_items.any? { |inv| inv.amount == machine_price }) - assert(invoice_items.all?(&:check_footprint)) - - # users_credits assertions - users_credit = UsersCredit.last - - assert_equal @user_with_subscription, users_credit.user - assert_equal [reservation.slots.count, plan.machine_credits.find_by(creditable_id: machine.id).hours].min, users_credit.hours_used - - # invoice assertions - item = InvoiceItem.find_by(object: reservation) - invoice = item.invoice - assert_invoice_pdf invoice - assert_not_nil invoice.debug_footprint - - refute invoice.payment_gateway_object.blank? - refute invoice.total.blank? - assert invoice.check_footprint - - # notification - assert_not_empty Notification.where(attached_object: reservation) - end - - test 'user with subscription reserves the FIRST training with success' do - login_as(@user_with_subscription, scope: :user) - plan = @user_with_subscription.subscribed_plan - plan.update!(is_rolling: true) - - training = Training.joins(credits: :plan).where(credits: { plan: plan }).first - availability = training.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - - VCR.use_cassette('reservations_create_for_training_with_subscription_success') do - post '/api/local_payment/confirm_payment', - params: { - items: [ - { - reservation: { - reservable_id: training.id, - reservable_type: training.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - } - ] - }.to_json, headers: default_headers - end - - # general assertions - assert_equal 201, response.status - assert_equal reservations_count + 1, Reservation.count - assert_equal invoice_count + 1, Invoice.count - assert_equal invoice_items_count + 1, InvoiceItem.count - - # subscription assertions - assert_equal 1, @user_with_subscription.subscriptions.count - assert_not_nil @user_with_subscription.subscribed_plan - assert_equal plan.id, @user_with_subscription.subscribed_plan.id - - # reservation assertions - reservation = Reservation.last - - assert reservation.original_invoice - assert_equal 1, reservation.original_invoice.invoice_items.count - - # invoice_items - invoice_item = InvoiceItem.last - - assert_equal 0, invoice_item.amount # amount is 0 because this training is a credited training with that plan - assert invoice_item.check_footprint - - # invoice assertions - item = InvoiceItem.find_by(object: reservation) - invoice = item.invoice - assert_invoice_pdf invoice - assert_not_nil invoice.debug_footprint - - assert invoice.payment_gateway_object.blank? - refute invoice.total.blank? - assert invoice.check_footprint - - # notification - assert_not_empty Notification.where(attached_object: reservation) - - # check that user subscription were extended - assert_equal reservation.slots.first.start_at + plan.duration, @user_with_subscription.subscription.expired_at - end - - test 'user reserves a machine and pay by wallet with success' do - @vlonchamp = User.find_by(username: 'vlonchamp') - login_as(@vlonchamp, scope: :user) - - machine = Machine.find(6) - availability = machine.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - users_credit_count = UsersCredit.count - wallet_transactions_count = WalletTransaction.count - - VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - customer_id: @vlonchamp.id, - items: [ - { - reservation: { - reservable_id: machine.id, - reservable_type: machine.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - } - ] - } - }.to_json, headers: default_headers - end - - @vlonchamp.wallet.reload - - # general assertions - assert_equal 201, response.status - assert_equal reservations_count + 1, Reservation.count - assert_equal invoice_count + 1, Invoice.count - assert_equal invoice_items_count + 1, InvoiceItem.count - assert_equal users_credit_count, UsersCredit.count - assert_equal wallet_transactions_count + 1, WalletTransaction.count - - # subscription assertions - assert_equal 0, @vlonchamp.subscriptions.count - assert_nil @vlonchamp.subscribed_plan - - # reservation assertions - reservation = Reservation.last - - assert reservation.original_invoice - assert_equal 1, reservation.original_invoice.invoice_items.count - - # invoice_items assertions - invoice_item = InvoiceItem.last - - assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount - assert invoice_item.check_footprint - - # invoice assertions - item = InvoiceItem.find_by(object: reservation) - invoice = item.invoice - assert_invoice_pdf invoice - assert_not_nil invoice.debug_footprint - - refute invoice.payment_gateway_object.blank? - refute invoice.total.blank? - assert invoice.check_footprint - - # notification - assert_not_empty Notification.where(attached_object: reservation) - - # wallet - assert_equal 0, @vlonchamp.wallet.amount - assert_equal 2, @vlonchamp.wallet.wallet_transactions.count - transaction = @vlonchamp.wallet.wallet_transactions.last - assert_equal 'debit', transaction.transaction_type - assert_equal 10, transaction.amount - assert_equal invoice.wallet_amount / 100.0, transaction.amount - end - - test 'user reserves a training and a subscription by wallet with success' do - @vlonchamp = User.find_by(username: 'vlonchamp') - login_as(@vlonchamp, scope: :user) - - training = Training.first - availability = training.availabilities.first - plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit') - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - wallet_transactions_count = WalletTransaction.count - - VCR.use_cassette('reservations_create_for_training_and_plan_by_pay_wallet_success') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - items: [ - { - reservation: { - reservable_id: training.id, - reservable_type: training.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - }, - { - subscription: { - plan_id: plan.id - } - } - ] - } - }.to_json, headers: default_headers - end - - @vlonchamp.wallet.reload - - # general assertions - assert_equal 201, response.status - assert_equal reservations_count + 1, Reservation.count - assert_equal invoice_count + 1, Invoice.count - assert_equal invoice_items_count + 2, InvoiceItem.count - assert_equal wallet_transactions_count + 1, WalletTransaction.count - - # subscription assertions - assert_equal 1, @vlonchamp.subscriptions.count - assert_not_nil @vlonchamp.subscribed_plan - assert_equal plan.id, @vlonchamp.subscribed_plan.id - - # reservation assertions - reservation = Reservation.last - - assert reservation.original_invoice - assert_equal 2, reservation.original_invoice.invoice_items.count - - # invoice assertions - item = InvoiceItem.find_by(object: reservation) - invoice = item.invoice - assert_invoice_pdf invoice - assert_not_nil invoice.debug_footprint - - refute invoice.payment_gateway_object.blank? - refute invoice.total.blank? - assert_equal invoice.total, 2000 - assert invoice.check_footprint - - # notification - assert_not_empty Notification.where(attached_object: reservation) - - # wallet - assert_equal 0, @vlonchamp.wallet.amount - assert_equal 2, @vlonchamp.wallet.wallet_transactions.count - transaction = @vlonchamp.wallet.wallet_transactions.last - assert_equal 'debit', transaction.transaction_type - assert_equal 10, transaction.amount - assert_equal invoice.wallet_amount / 100.0, transaction.amount - end - - test 'user reserves a machine and a subscription using a coupon with success' do - login_as(@user_without_subscription, scope: :user) - - machine = Machine.find(6) - plan = Plan.find(4) - availability = machine.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - subscriptions_count = Subscription.count - users_credit_count = UsersCredit.count - - VCR.use_cassette('reservations_machine_and_plan_using_coupon_success') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - items: [ - { - reservation: { - reservable_id: machine.id, - reservable_type: machine.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - }, - { - subscription: { - plan_id: plan.id - } - } - ], - coupon_code: 'SUNNYFABLAB' - } - }.to_json, headers: default_headers - end - - # general assertions - assert_equal 201, response.status - assert_equal reservations_count + 1, Reservation.count - assert_equal invoice_count + 1, Invoice.count - assert_equal invoice_items_count + 2, InvoiceItem.count - assert_equal users_credit_count, UsersCredit.count - assert_equal subscriptions_count + 1, Subscription.count - - # subscription assertions - assert_equal 1, @user_without_subscription.subscriptions.count - assert_not_nil @user_without_subscription.subscribed_plan - assert_equal plan.id, @user_without_subscription.subscribed_plan.id - - # reservation assertions - reservation = Reservation.last - - assert reservation.original_invoice - assert_equal 2, reservation.original_invoice.invoice_items.count - - # invoice assertions - item = InvoiceItem.find_by(object: reservation) - invoice = item.invoice - assert_invoice_pdf invoice - assert_not_nil invoice.debug_footprint - - refute invoice.payment_gateway_object.blank? - refute invoice.total.blank? - assert invoice.check_footprint - - # invoice_items assertions - ## reservation - 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.find_by(object_type: Subscription.name) - - assert_not_nil subscription_item - - subscription = subscription_item.object - - assert_equal subscription_item.amount, plan.amount - assert_equal subscription.plan_id, plan.id - assert subscription_item.check_footprint - - VCR.use_cassette('reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe') do - stp_intent = invoice.payment_gateway_object.gateway_object.retrieve - assert_equal stp_intent.amount, invoice.total - end - - # notifications - assert_not_empty Notification.where(attached_object: reservation) - assert_not_empty Notification.where(attached_object: subscription) - end - - test 'user reserves a training with an expired coupon with error' do - login_as(@user_without_subscription, scope: :user) - - training = Training.find(1) - availability = training.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - notifications_count = Notification.count - - VCR.use_cassette('reservations_training_with_expired_coupon_error') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - customer_id: @user_without_subscription.id, - items: [ - { - reservation: { - reservable_id: training.id, - reservable_type: training.class.name, - card_token: stripe_payment_method, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - } - ], - coupon_code: 'XMAS10' - } - }.to_json, headers: default_headers - end - - # general assertions - assert_equal 422, response.status - assert_equal reservations_count, Reservation.count - assert_equal invoice_count, Invoice.count - assert_equal invoice_items_count, InvoiceItem.count - assert_equal notifications_count, Notification.count - - # subscription assertions - assert_equal 0, @user_without_subscription.subscriptions.count - assert_nil @user_without_subscription.subscribed_plan - end - - - test 'user reserves a training and a subscription with payment schedule' do - login_as(@user_without_subscription, scope: :user) - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - subscriptions_count = Subscription.count - users_credit_count = UsersCredit.count - payment_schedule_count = PaymentSchedule.count - payment_schedule_items_count = PaymentScheduleItem.count - - training = Training.find(1) - availability = training.availabilities.first - plan = Plan.find_by(group_id: @user_without_subscription.group.id, type: 'Plan', base_name: 'Abonnement mensualisable') - - VCR.use_cassette('reservations_training_subscription_with_payment_schedule') do - post '/api/stripe/setup_subscription', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - items: [ - { - reservation: { - reservable_id: training.id, - reservable_type: training.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - }, - { - subscription: { - plan_id: plan.id - } - } - ], - payment_schedule: true, - payment_method: 'cart' - } - }.to_json, headers: default_headers - - # Check response format & status - assert_equal 201, response.status, response.body - assert_equal Mime[:json], response.content_type - - # Check the response - sub = json_response(response.body) - assert_not_nil sub[:id] - 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 users_credit_count, UsersCredit.count, "user's credits count has changed but it shouldn't" - 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' - - # get the objects - reservation = Reservation.last - payment_schedule = PaymentSchedule.last - - # subscription assertions - assert_equal 1, @user_without_subscription.subscriptions.count - assert_not_nil @user_without_subscription.subscribed_plan, "user's subscribed plan was not found" - 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" - - # reservation assertions - assert reservation.original_payment_schedule - assert_equal payment_schedule.main_object.object, reservation - - # Check the answer - result = json_response(response.body) - assert_equal payment_schedule.id, result[:id], 'payment schedule id does not match' - subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object - assert_equal plan.id, subscription.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 - post '/api/stripe/setup_subscription', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - items: [ - { - reservation: { - reservable_id: machine.id, - reservable_type: machine.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - }, - { - subscription: { - plan_id: plan.id - } - } - ], - payment_schedule: true, - payment_method: 'card', - coupon_code: 'GIME3EUR' - } - }.to_json, headers: default_headers - - # Check response format & status - assert_equal 201, response.status, response.body - assert_equal Mime[:json], response.content_type - - # Check the response - res = json_response(response.body) - assert_not_nil res[:id] - end - - 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.original_payment_schedule - assert_equal payment_schedule.main_object.object, reservation - - # payment schedule assertions - assert_not_nil payment_schedule.reference - assert_equal 'card', payment_schedule.payment_method - assert_equal 2, payment_schedule.payment_gateway_objects.count - assert_not_nil payment_schedule.gateway_payment_mean - 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 - result = json_response(response.body) - assert_equal payment_schedule.id, result[:id], 'payment schedule id does not match' - subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object - assert_equal plan.id, subscription.plan_id, 'subscribed plan does not match' - end - - test 'user reserves a space with success' do - login_as(@user_without_subscription, scope: :user) - - space = Space.first - availability = space.availabilities.first - - reservations_count = Reservation.count - invoice_count = Invoice.count - invoice_items_count = InvoiceItem.count - users_credit_count = UsersCredit.count - subscriptions_count = Subscription.count - - VCR.use_cassette('reservations_create_for_space_success') do - post '/api/stripe/confirm_payment', - params: { - payment_method_id: stripe_payment_method, - cart_items: { - items: [ - { - reservation: { - reservable_id: space.id, - reservable_type: space.class.name, - slots_reservations_attributes: [ - { - slot_id: availability.slots.first.id - } - ] - } - } - ] - } - }.to_json, headers: default_headers - end - - # general assertions - assert_equal 201, response.status - assert_equal reservations_count + 1, Reservation.count - assert_equal invoice_count + 1, Invoice.count - assert_equal invoice_items_count + 1, InvoiceItem.count - assert_equal users_credit_count, UsersCredit.count - assert_equal subscriptions_count, Subscription.count - - # subscription assertions - assert_equal 0, @user_without_subscription.subscriptions.count - assert_nil @user_without_subscription.subscribed_plan - - # reservation assertions - reservation = Reservation.last - - assert reservation.original_invoice - assert_equal 1, reservation.original_invoice.invoice_items.count - - # invoice_items assertions - invoice_item = InvoiceItem.last - - assert_equal space.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount - assert invoice_item.check_footprint - - # invoice assertions - item = InvoiceItem.find_by(object: reservation) - invoice = item.invoice - assert_invoice_pdf invoice - assert_not_nil invoice.debug_footprint - - refute invoice.payment_gateway_object.blank? - refute invoice.total.blank? - assert invoice.check_footprint - - # notification - assert_not_empty Notification.where(attached_object: reservation) - end -end diff --git a/test/integration/reservations/pay_with_wallet_test.rb b/test/integration/reservations/pay_with_wallet_test.rb new file mode 100644 index 000000000..67c6d5a76 --- /dev/null +++ b/test/integration/reservations/pay_with_wallet_test.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Reservations; end + +class Reservations::PayWithWalletTest < ActionDispatch::IntegrationTest + setup do + @vlonchamp = User.find_by(username: 'vlonchamp') + end + + test 'user reserves a machine and pay by wallet with success' do + login_as(@vlonchamp, scope: :user) + + machine = Machine.find(6) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + wallet_transactions_count = WalletTransaction.count + + VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + customer_id: @vlonchamp.id, + items: [ + { + reservation: { + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + @vlonchamp.wallet.reload + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal wallet_transactions_count + 1, WalletTransaction.count + + # subscription assertions + assert_equal 0, @vlonchamp.subscriptions.count + assert_nil @vlonchamp.subscribed_plan + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count + + # invoice_items assertions + invoice_item = InvoiceItem.last + + assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount + assert invoice_item.check_footprint + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + # wallet + assert_equal 0, @vlonchamp.wallet.amount + assert_equal 2, @vlonchamp.wallet.wallet_transactions.count + transaction = @vlonchamp.wallet.wallet_transactions.last + assert_equal 'debit', transaction.transaction_type + assert_equal 10, transaction.amount + assert_equal invoice.wallet_amount / 100.0, transaction.amount + end + + test 'user reserves a training and a subscription by wallet with success' do + login_as(@vlonchamp, scope: :user) + + training = Training.first + availability = training.availabilities.first + plan = Plan.find_by(group_id: @vlonchamp.group.id, type: 'Plan', base_name: 'Mensuel tarif réduit') + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + wallet_transactions_count = WalletTransaction.count + + VCR.use_cassette('reservations_create_for_training_and_plan_by_pay_wallet_success') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: training.id, + reservable_type: training.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + }, + { + subscription: { + plan_id: plan.id + } + } + ] + } + }.to_json, headers: default_headers + end + + @vlonchamp.wallet.reload + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 2, InvoiceItem.count + assert_equal wallet_transactions_count + 1, WalletTransaction.count + + # subscription assertions + assert_equal 1, @vlonchamp.subscriptions.count + assert_not_nil @vlonchamp.subscribed_plan + assert_equal plan.id, @vlonchamp.subscribed_plan.id + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert_equal invoice.total, 2000 + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + # wallet + assert_equal 0, @vlonchamp.wallet.amount + assert_equal 2, @vlonchamp.wallet.wallet_transactions.count + transaction = @vlonchamp.wallet.wallet_transactions.last + assert_equal 'debit', transaction.transaction_type + assert_equal 10, transaction.amount + assert_equal invoice.wallet_amount / 100.0, transaction.amount + 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 + post '/api/stripe/setup_subscription', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + }, + { + subscription: { + plan_id: plan.id + } + } + ], + payment_schedule: true, + payment_method: 'card', + coupon_code: 'GIME3EUR' + } + }.to_json, headers: default_headers + + # Check response format & status + assert_equal 201, response.status, response.body + assert_equal Mime[:json], response.content_type + + # Check the response + res = json_response(response.body) + assert_not_nil res[:id] + end + + 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.original_payment_schedule + assert_equal payment_schedule.main_object.object, reservation + + # payment schedule assertions + assert_not_nil payment_schedule.reference + assert_equal 'card', payment_schedule.payment_method + assert_equal 2, payment_schedule.payment_gateway_objects.count + assert_not_nil payment_schedule.gateway_payment_mean + assert_not_nil payment_schedule.wallet_transaction + assert_equal CouponService.new.apply(payment_schedule.ordered_items.first.amount, payment_schedule.coupon, user.id), + 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 + result = json_response(response.body) + assert_equal payment_schedule.id, result[:id], 'payment schedule id does not match' + subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object + assert_equal plan.id, subscription.plan_id, 'subscribed plan does not match' + end +end diff --git a/test/integration/reservations/reserve_machine_test.rb b/test/integration/reservations/reserve_machine_test.rb new file mode 100644 index 000000000..e9237d1da --- /dev/null +++ b/test/integration/reservations/reserve_machine_test.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Reservations; end + +class Reservations::ReserveMachineTest < ActionDispatch::IntegrationTest + setup do + @user_without_subscription = User.members.without_subscription.first + end + + test 'user without subscription reserves a machine with success' do + login_as(@user_without_subscription, scope: :user) + + machine = Machine.find(6) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + subscriptions_count = Subscription.count + + VCR.use_cassette('reservations_create_for_machine_without_subscription_success') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal subscriptions_count, Subscription.count + + # subscription assertions + assert_equal 0, @user_without_subscription.subscriptions.count + assert_nil @user_without_subscription.subscribed_plan + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count + + # invoice_items assertions + invoice_item = InvoiceItem.last + + assert_equal machine.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount + assert invoice_item.check_footprint + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + end + + test 'user without subscription reserves a machine with error' do + login_as(@user_without_subscription, scope: :user) + + machine = Machine.find(6) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + notifications_count = Notification.count + + VCR.use_cassette('reservations_create_for_machine_without_subscription_error') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method(error: :card_declined), + cart_items: { + items: [ + { + reservation: { + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + # Check response format & status + assert_equal 200, response.status, "API does not return the expected status. #{response.body}" + assert_equal Mime[:json], response.content_type + + # Check the error was handled + assert_match(/Your card was declined/, response.body) + + # Check the subscription wasn't taken + assert_equal reservations_count, Reservation.count + assert_equal invoice_count, Invoice.count + assert_equal invoice_items_count, InvoiceItem.count + assert_equal notifications_count, Notification.count + + # subscription assertions + assert_equal 0, @user_without_subscription.subscriptions.count + assert_nil @user_without_subscription.subscribed_plan + end + + test 'user reserves a machine and a subscription using a coupon with success' do + login_as(@user_without_subscription, scope: :user) + + machine = Machine.find(6) + plan = Plan.find(4) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + subscriptions_count = Subscription.count + users_credit_count = UsersCredit.count + + VCR.use_cassette('reservations_machine_and_plan_using_coupon_success') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + }, + { + subscription: { + plan_id: plan.id + } + } + ], + coupon_code: 'SUNNYFABLAB' + } + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 2, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal subscriptions_count + 1, Subscription.count + + # subscription assertions + assert_equal 1, @user_without_subscription.subscriptions.count + assert_not_nil @user_without_subscription.subscribed_plan + assert_equal plan.id, @user_without_subscription.subscribed_plan.id + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # invoice_items assertions + ## reservation + 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.find_by(object_type: Subscription.name) + + assert_not_nil subscription_item + + subscription = subscription_item.object + + assert_equal subscription_item.amount, plan.amount + assert_equal subscription.plan_id, plan.id + assert subscription_item.check_footprint + + VCR.use_cassette('reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe') do + stp_intent = invoice.payment_gateway_object.gateway_object.retrieve + assert_equal stp_intent.amount, invoice.total + end + + # notifications + assert_not_empty Notification.where(attached_object: reservation) + assert_not_empty Notification.where(attached_object: subscription) + end +end diff --git a/test/integration/reservations/reserve_space_test.rb b/test/integration/reservations/reserve_space_test.rb new file mode 100644 index 000000000..d54f71c89 --- /dev/null +++ b/test/integration/reservations/reserve_space_test.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Reservations; end + +class Reservations::ReserveSpaceTest < ActionDispatch::IntegrationTest + setup do + @user_without_subscription = User.members.without_subscription.first + end + + test 'user reserves a space with success' do + login_as(@user_without_subscription, scope: :user) + + space = Space.first + availability = space.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + subscriptions_count = Subscription.count + + VCR.use_cassette('reservations_create_for_space_success') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: space.id, + reservable_type: space.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal subscriptions_count, Subscription.count + + # subscription assertions + assert_equal 0, @user_without_subscription.subscriptions.count + assert_nil @user_without_subscription.subscribed_plan + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count + + # invoice_items assertions + invoice_item = InvoiceItem.last + + assert_equal space.prices.find_by(group_id: @user_without_subscription.group_id, plan_id: nil).amount, invoice_item.amount + assert invoice_item.check_footprint + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + end +end diff --git a/test/integration/reservations/reserve_training_test.rb b/test/integration/reservations/reserve_training_test.rb new file mode 100644 index 000000000..7ad91c2e6 --- /dev/null +++ b/test/integration/reservations/reserve_training_test.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Reservations; end + +class Reservations::ReserveTrainingTest < ActionDispatch::IntegrationTest + setup do + @user_without_subscription = User.members.without_subscription.first + end + + test 'user without subscription reserves a training with success' do + login_as(@user_without_subscription, scope: :user) + + training = Training.first + availability = training.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + + VCR.use_cassette('reservations_create_for_training_without_subscription_success') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: training.id, + reservable_type: training.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + + # subscription assertions + assert_equal 0, @user_without_subscription.subscriptions.count + assert_nil @user_without_subscription.subscribed_plan + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count + + # invoice_items + invoice_item = InvoiceItem.last + + assert_equal invoice_item.amount, training.amount_by_group(@user_without_subscription.group_id).amount + assert invoice_item.check_footprint + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + end + + test 'user reserves a training with an expired coupon with error' do + login_as(@user_without_subscription, scope: :user) + + training = Training.find(1) + availability = training.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + notifications_count = Notification.count + + VCR.use_cassette('reservations_training_with_expired_coupon_error') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + customer_id: @user_without_subscription.id, + items: [ + { + reservation: { + reservable_id: training.id, + reservable_type: training.class.name, + card_token: stripe_payment_method, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ], + coupon_code: 'XMAS10' + } + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 422, response.status + assert_equal reservations_count, Reservation.count + assert_equal invoice_count, Invoice.count + assert_equal invoice_items_count, InvoiceItem.count + assert_equal notifications_count, Notification.count + + # subscription assertions + assert_equal 0, @user_without_subscription.subscriptions.count + assert_nil @user_without_subscription.subscribed_plan + end + + test 'user reserves a training and a subscription with payment schedule' do + login_as(@user_without_subscription, scope: :user) + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + subscriptions_count = Subscription.count + users_credit_count = UsersCredit.count + payment_schedule_count = PaymentSchedule.count + payment_schedule_items_count = PaymentScheduleItem.count + + training = Training.find(1) + availability = training.availabilities.first + plan = Plan.find_by(group_id: @user_without_subscription.group.id, type: 'Plan', base_name: 'Abonnement mensualisable') + + VCR.use_cassette('reservations_training_subscription_with_payment_schedule') do + post '/api/stripe/setup_subscription', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: training.id, + reservable_type: training.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + }, + { + subscription: { + plan_id: plan.id + } + } + ], + payment_schedule: true, + payment_method: 'cart' + } + }.to_json, headers: default_headers + + # Check response format & status + assert_equal 201, response.status, response.body + assert_equal Mime[:json], response.content_type + + # Check the response + sub = json_response(response.body) + assert_not_nil sub[:id] + 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 users_credit_count, UsersCredit.count, "user's credits count has changed but it shouldn't" + 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' + + # get the objects + reservation = Reservation.last + payment_schedule = PaymentSchedule.last + + # subscription assertions + assert_equal 1, @user_without_subscription.subscriptions.count + assert_not_nil @user_without_subscription.subscribed_plan, "user's subscribed plan was not found" + 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" + + # reservation assertions + assert reservation.original_payment_schedule + assert_equal payment_schedule.main_object.object, reservation + + # Check the answer + result = json_response(response.body) + assert_equal payment_schedule.id, result[:id], 'payment schedule id does not match' + subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object + assert_equal plan.id, subscription.plan_id, 'subscribed plan does not match' + end +end diff --git a/test/integration/reservations/with_subscription_test.rb b/test/integration/reservations/with_subscription_test.rb new file mode 100644 index 000000000..02a3c00d7 --- /dev/null +++ b/test/integration/reservations/with_subscription_test.rb @@ -0,0 +1,249 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Reservations; end + +class Reservations::WithSubscriptionTest < ActionDispatch::IntegrationTest + setup do + @user_with_subscription = User.members.with_subscription.second + end + + test 'user with subscription reserves a machine with success' do + login_as(@user_with_subscription, scope: :user) + + plan = @user_with_subscription.subscribed_plan + machine = Machine.find(6) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + + VCR.use_cassette('reservations_create_for_machine_with_subscription_success') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + }, + { + slot_id: availability.slots.last.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 2, InvoiceItem.count + assert_equal users_credit_count + 1, UsersCredit.count + + # subscription assertions + assert_equal 1, @user_with_subscription.subscriptions.count + assert_not_nil @user_with_subscription.subscribed_plan + assert_equal plan.id, @user_with_subscription.subscribed_plan.id + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 2, reservation.original_invoice.invoice_items.count + + # invoice_items assertions + invoice_items = InvoiceItem.last(2) + machine_price = machine.prices.find_by(group_id: @user_with_subscription.group_id, plan_id: plan.id).amount + + assert(invoice_items.any? { |inv| inv.amount.zero? }) + assert(invoice_items.any? { |inv| inv.amount == machine_price }) + assert(invoice_items.all?(&:check_footprint)) + + # users_credits assertions + users_credit = UsersCredit.last + + assert_equal @user_with_subscription, users_credit.user + assert_equal [reservation.slots.count, plan.machine_credits.find_by(creditable_id: machine.id).hours].min, users_credit.hours_used + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + end + + test 'user with subscription reserves the FIRST training with success' do + login_as(@user_with_subscription, scope: :user) + plan = @user_with_subscription.subscribed_plan + plan.update!(is_rolling: true) + + training = Training.joins(credits: :plan).where(credits: { plan: plan }).first + availability = training.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + + VCR.use_cassette('reservations_create_for_training_with_subscription_success') do + post '/api/local_payment/confirm_payment', + params: { + items: [ + { + reservation: { + reservable_id: training.id, + reservable_type: training.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + + # subscription assertions + assert_equal 1, @user_with_subscription.subscriptions.count + assert_not_nil @user_with_subscription.subscribed_plan + assert_equal plan.id, @user_with_subscription.subscribed_plan.id + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count + + # invoice_items + invoice_item = InvoiceItem.last + + assert_equal 0, invoice_item.amount # amount is 0 because this training is a credited training with that plan + assert invoice_item.check_footprint + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + # check that user subscription were extended + assert_equal reservation.slots.first.start_at + plan.duration, @user_with_subscription.subscription.expired_at + end + + test 'user reserves a machine and pay by wallet with success' do + @vlonchamp = User.find_by(username: 'vlonchamp') + login_as(@vlonchamp, scope: :user) + + machine = Machine.find(6) + availability = machine.availabilities.first + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + wallet_transactions_count = WalletTransaction.count + + VCR.use_cassette('reservations_create_for_machine_and_pay_wallet_success') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + customer_id: @vlonchamp.id, + items: [ + { + reservation: { + reservable_id: machine.id, + reservable_type: machine.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + @vlonchamp.wallet.reload + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal wallet_transactions_count + 1, WalletTransaction.count + + # subscription assertions + assert_equal 0, @vlonchamp.subscriptions.count + assert_nil @vlonchamp.subscribed_plan + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count + + # invoice_items assertions + invoice_item = InvoiceItem.last + + assert_equal machine.prices.find_by(group_id: @vlonchamp.group_id, plan_id: nil).amount, invoice_item.amount + assert invoice_item.check_footprint + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + # wallet + assert_equal 0, @vlonchamp.wallet.amount + assert_equal 2, @vlonchamp.wallet.wallet_transactions.count + transaction = @vlonchamp.wallet.wallet_transactions.last + assert_equal 'debit', transaction.transaction_type + assert_equal 10, transaction.amount + assert_equal invoice.wallet_amount / 100.0, transaction.amount + end +end From f0df811406f8db384fcda787d5d28c80d6f6a382 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 27 Sep 2022 17:22:39 +0200 Subject: [PATCH 14/17] Version 5.4.20 --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a67856ec..47e0547e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # Changelog Fab-manager +## v5.4.20 2022 September 27 + - Fix a bug: unable to show the daily view of the public agenda, if it contains trainings or events - Fix a bug: plan's categories descriptions are not shown - Fix a bug: groups without plans are shown but empty - Fix a bug: unable to display the payment schedules management interface ## v5.4.19 2022 September 13 + - Fix a bug: computing the wallet amount to debit ignores the applied coupon ## v5.4.18 2022 September 12 diff --git a/package.json b/package.json index 2270235c7..318be1130 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.19", + "version": "5.4.20", "description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.", "keywords": [ "fablab", From 51aa9670018cc6aa173b4ce2d9fda5d6a4ed82be Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 3 Oct 2022 14:22:15 +0200 Subject: [PATCH 15/17] (feat) dismiss a user to a lower privileged role --- CHANGELOG.md | 2 + app/controllers/api/members_controller.rb | 92 ++++--------- app/frontend/src/javascript/api/member.ts | 7 +- .../components/user/change-role-modal.tsx | 129 ++++++++++++++++++ .../javascript/controllers/admin/members.js | 14 ++ app/frontend/src/stylesheets/application.scss | 1 + .../stylesheets/modules/form/form-select.scss | 9 ++ .../templates/admin/members/edit.html | 3 +- app/services/export_service.rb | 48 +++++++ app/services/members/members_service.rb | 38 ++++++ config/locales/app.admin.en.yml | 11 +- 11 files changed, 282 insertions(+), 72 deletions(-) create mode 100644 app/frontend/src/javascript/components/user/change-role-modal.tsx create mode 100644 app/frontend/src/stylesheets/modules/form/form-select.scss create mode 100644 app/services/export_service.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e0547e0..a570eb140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +- Ability to dismiss a user to a lower privileged role + ## v5.4.20 2022 September 27 - Fix a bug: unable to show the daily view of the public agenda, if it contains trainings or events diff --git a/app/controllers/api/members_controller.rb b/app/controllers/api/members_controller.rb index 5ca7f455c..12cb86743 100644 --- a/app/controllers/api/members_controller.rb +++ b/app/controllers/api/members_controller.rb @@ -18,15 +18,7 @@ class API::MembersController < API::ApiController end def last_subscribed - @query = User.active.with_role(:member) - .includes(:statistic_profile, profile: [:user_avatar]) - .where('is_allow_contact = true AND confirmed_at IS NOT NULL') - .order('created_at desc') - .limit(params[:last]) - - # remove unmerged profiles from list - @members = @query.to_a - @members.delete_if(&:need_completion?) + @query, @members = Members::MembersService.last_registered(params[:last]) @requested_attributes = ['profile'] render :index @@ -74,9 +66,7 @@ class API::MembersController < API::ApiController def export_subscriptions authorize :export - export = Export.where(category: 'users', export_type: 'subscriptions') - .where('created_at > ?', Subscription.maximum('updated_at')) - .last + export = ExportService.last_export('users/subscription') if export.nil? || !FileTest.exist?(export.file) @export = Export.new(category: 'users', export_type: 'subscriptions', user: current_user) if @export.save @@ -85,7 +75,7 @@ class API::MembersController < API::ApiController render json: @export.errors, status: :unprocessable_entity end else - send_file File.join(Rails.root, export.file), + send_file Rails.root.join(export.file), type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment' end @@ -95,9 +85,7 @@ class API::MembersController < API::ApiController def export_reservations authorize :export - export = Export.where(category: 'users', export_type: 'reservations') - .where('created_at > ?', Reservation.maximum('updated_at')) - .last + export = ExportService.last_export('users/reservations') if export.nil? || !FileTest.exist?(export.file) @export = Export.new(category: 'users', export_type: 'reservations', user: current_user) if @export.save @@ -106,7 +94,7 @@ class API::MembersController < API::ApiController render json: @export.errors, status: :unprocessable_entity end else - send_file File.join(Rails.root, export.file), + send_file Rails.root.join(export.file), type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment' end @@ -115,17 +103,7 @@ class API::MembersController < API::ApiController def export_members authorize :export - last_update = [ - User.members.maximum('updated_at'), - Profile.where(user_id: User.members).maximum('updated_at'), - InvoicingProfile.where(user_id: User.members).maximum('updated_at'), - StatisticProfile.where(user_id: User.members).maximum('updated_at'), - Subscription.maximum('updated_at') || DateTime.current - ].max - - export = Export.where(category: 'users', export_type: 'members') - .where('created_at > ?', last_update) - .last + export = ExportService.last_export('users/members') if export.nil? || !FileTest.exist?(export.file) @export = Export.new(category: 'users', export_type: 'members', user: current_user) if @export.save @@ -134,7 +112,7 @@ class API::MembersController < API::ApiController render json: @export.errors, status: :unprocessable_entity end else - send_file File.join(Rails.root, export.file), + send_file Rails.root.join(export.file), type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment' end @@ -158,8 +136,8 @@ class API::MembersController < API::ApiController else render json: @member.errors, status: :unprocessable_entity end - rescue DuplicateIndexError => error - render json: { error: t('members.please_input_the_authentication_code_sent_to_the_address', EMAIL: error.message) }, + rescue DuplicateIndexError => e + render json: { error: t('members.please_input_the_authentication_code_sent_to_the_address', EMAIL: e.message) }, status: :unprocessable_entity end else @@ -176,7 +154,6 @@ class API::MembersController < API::ApiController query = Members::ListService.list(query_params) @max_members = query.except(:offset, :limit, :order).count @members = query.to_a - end def search @@ -196,7 +173,7 @@ class API::MembersController < API::ApiController render json: { tours: [params[:tour]] } else tours = "#{@member.profile.tours} #{params[:tour]}" - @member.profile.update_attributes(tours: tours.strip) + @member.profile.update(tours: tours.strip) render json: { tours: @member.profile.tours.split } end @@ -205,31 +182,8 @@ class API::MembersController < API::ApiController def update_role authorize @member - # we do not allow dismissing a user to a lower role - if params[:role] == 'member' - render 403 and return if @member.role == 'admin' || @member.role == 'manager' - elsif params[:role] == 'manager' - render 403 and return if @member.role == 'admin' - end - - # do nothing if the role does not change - render json: @member and return if params[:role] == @member.role - - ex_role = @member.role.to_sym - @member.remove_role ex_role - @member.add_role params[:role] - - # if the new role is 'admin', then change the group to the admins group - @member.update_attributes(group_id: Group.find_by(slug: 'admins').id) if params[:role] == 'admin' - - NotificationCenter.call type: 'notify_user_role_update', - receiver: @member, - attached_object: @member - - NotificationCenter.call type: 'notify_admins_role_update', - receiver: User.admins_and_managers, - attached_object: @member, - meta_data: { ex_role: ex_role } + service = Members::MembersService.new(@member) + service.update_role(params[:role], params[:group_id]) render json: @member end @@ -265,12 +219,14 @@ class API::MembersController < API::ApiController profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, - user_avatar_attributes: %i[id attachment destroy]], + { user_avatar_attributes: %i[id attachment destroy] }], invoicing_profile_attributes: [ :id, :organization, - address_attributes: %i[id address], - organization_attributes: [:id, :name, address_attributes: %i[id address]], - user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id] + { + address_attributes: %i[id address], + organization_attributes: [:id, :name, { address_attributes: %i[id address] }], + user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id] + } ], statistic_profile_attributes: %i[id gender birthday]) @@ -280,14 +236,16 @@ class API::MembersController < API::ApiController profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, - user_avatar_attributes: %i[id attachment destroy]], + { user_avatar_attributes: %i[id attachment destroy] }], invoicing_profile_attributes: [ :id, :organization, - address_attributes: %i[id address], - organization_attributes: [:id, :name, address_attributes: %i[id address]], - user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id] + { + address_attributes: %i[id address], + organization_attributes: [:id, :name, { address_attributes: %i[id address] }], + user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id] + } ], - statistic_profile_attributes: [:id, :gender, :birthday, training_ids: []]) + statistic_profile_attributes: [:id, :gender, :birthday, { training_ids: [] }]) end end diff --git a/app/frontend/src/javascript/api/member.ts b/app/frontend/src/javascript/api/member.ts index 0c3697c18..9970bef03 100644 --- a/app/frontend/src/javascript/api/member.ts +++ b/app/frontend/src/javascript/api/member.ts @@ -1,7 +1,7 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; import { serialize } from 'object-to-formdata'; -import { User, UserIndexFilter } from '../models/user'; +import { User, UserIndexFilter, UserRole } from '../models/user'; export default class MemberAPI { static async list (filters: UserIndexFilter): Promise> { @@ -35,6 +35,11 @@ export default class MemberAPI { return res?.data; } + static async updateRole (user: User, role: UserRole, groupId?: number): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/members/${user.id}/update_role`, { role, group_id: groupId }); + return res?.data; + } + static async current (): Promise { const res: AxiosResponse = await apiClient.get('/api/members/current'); return res?.data; diff --git a/app/frontend/src/javascript/components/user/change-role-modal.tsx b/app/frontend/src/javascript/components/user/change-role-modal.tsx new file mode 100644 index 000000000..3f844f643 --- /dev/null +++ b/app/frontend/src/javascript/components/user/change-role-modal.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from 'react'; +import { FabModal, ModalSize } from '../base/fab-modal'; +import { User, UserRole } from '../../models/user'; +import { IApplication } from '../../models/application'; +import { Loader } from '../base/loader'; +import { react2angular } from 'react2angular'; +import { useTranslation } from 'react-i18next'; +import { HtmlTranslate } from '../base/html-translate'; +import { useForm } from 'react-hook-form'; +import MemberAPI from '../../api/member'; +import { FormSelect } from '../form/form-select'; +import { Group } from '../../models/group'; +import GroupAPI from '../../api/group'; + +declare const Application: IApplication; + +interface ChangeRoleModalProps { + isOpen: boolean, + toggleModal: () => void, + user: User, + onError: (message: string) => void, + onSuccess: (message: string) => void, +} + +interface RoleFormData { + role: UserRole, + groupId?: number +} + +/** + * Option format, expected by react-select + * @see https://github.com/JedWatson/react-select + */ +type selectRoleOption = { value: UserRole, label: string, isDisabled: boolean }; +type selectGroupOption = { value: number, label: string }; + +/** + * This modal dialog allows to change the current role of the given user + */ +export const ChangeRoleModal: React.FC = ({ isOpen, toggleModal, user, onSuccess, onError }) => { + const { t } = useTranslation('admin'); + const { control, handleSubmit } = useForm({ defaultValues: { groupId: user.group_id } }); + + const [groups, setGroups] = useState>([]); + const [selectedRole, setSelectedRole] = useState(user.role); + + useEffect(() => { + GroupAPI.index({ disabled: false, admins: false }).then(setGroups).catch(onError); + }, []); + + /** + * Handle the form submission: update the role on the API + */ + const onSubmit = (data: RoleFormData) => { + MemberAPI.updateRole(user, data.role, data.groupId).then(res => { + onSuccess( + t( + 'app.admin.change_role_modal.role_changed', + { OLD: t(`app.admin.change_role_modal.${user.role}`), NEW: t(`app.admin.change_role_modal.${res.role}`) } + ) + ); + toggleModal(); + }).catch(err => onError(t('app.admin.change_role_modal.error_while_changing_role') + err)); + }; + + /** + * Callback triggered when the user changes the selected role in the dropdown selection list + */ + const onRoleSelect = (data: UserRole) => { + setSelectedRole(data); + }; + + /** + * Return the various available roles for the select input + */ + const buildRolesOptions = (): Array => { + return [ + { value: 'admin' as UserRole, label: t('app.admin.change_role_modal.admin'), isDisabled: user.role === 'admin' }, + { value: 'manager' as UserRole, label: t('app.admin.change_role_modal.manager'), isDisabled: user.role === 'manager' }, + { value: 'member' as UserRole, label: t('app.admin.change_role_modal.member'), isDisabled: user.role === 'member' } + ]; + }; + + /** + * Return the various available groups for the select input + */ + const buildGroupsOptions = (): Array => { + return groups.map(group => { + return { value: group.id, label: group.name }; + }); + }; + + return ( + + +
+ + {selectedRole !== 'admin' && + } + +
+ ); +}; + +const ChangeRoleModalWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +Application.Components.component('changeRoleModal', react2angular(ChangeRoleModalWrapper, ['isOpen', 'toggleModal', 'user', 'onError', 'onSuccess'])); diff --git a/app/frontend/src/javascript/controllers/admin/members.js b/app/frontend/src/javascript/controllers/admin/members.js index ad0f343f7..ab1bdae22 100644 --- a/app/frontend/src/javascript/controllers/admin/members.js +++ b/app/frontend/src/javascript/controllers/admin/members.js @@ -724,6 +724,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', // modal dialog to take a new subscription $scope.isOpenSubscribeModal = false; + // modal dialog to change the user's role + $scope.isOpenChangeRoleModal = false; + /** * Open a modal dialog asking for confirmation to change the role of the given user * @returns {*} @@ -800,6 +803,17 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', $scope.$apply(); }, 50); }; + + /** + * Opens/closes the modal dialog to change the user's role + */ + $scope.toggleChangeRoleModal = () => { + setTimeout(() => { + $scope.isOpenChangeRoleModal = !$scope.isOpenChangeRoleModal; + $scope.$apply(); + }, 0); + }; + /** * Callback triggered if the subscription was successfully extended */ diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index 14723c579..e7d0a4efe 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -37,6 +37,7 @@ @import "modules/form/abstract-form-item"; @import "modules/form/form-input"; @import "modules/form/form-rich-text"; +@import "modules/form/form-select"; @import "modules/form/form-switch"; @import "modules/group/change-group"; @import "modules/machines/machine-card"; diff --git a/app/frontend/src/stylesheets/modules/form/form-select.scss b/app/frontend/src/stylesheets/modules/form/form-select.scss new file mode 100644 index 000000000..73aa63a33 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/form/form-select.scss @@ -0,0 +1,9 @@ +.form-select { + .rs__menu .rs__menu-list { + .rs__option { + &--is-disabled { + color: var(--gray-hard-lightest); + } + } + } +} diff --git a/app/frontend/templates/admin/members/edit.html b/app/frontend/templates/admin/members/edit.html index 389ec4a9d..905924ff5 100644 --- a/app/frontend/templates/admin/members/edit.html +++ b/app/frontend/templates/admin/members/edit.html @@ -21,9 +21,10 @@
-
+
role icon{{ 'app.admin.members_edit.change_role' }}
+
diff --git a/app/services/export_service.rb b/app/services/export_service.rb new file mode 100644 index 000000000..1e4bd3ebe --- /dev/null +++ b/app/services/export_service.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Provides helper methods for Exports resources and properties +class ExportService + class << self + # Check if the last export of the provided type is still accurate or if it must be regenerated + def last_export(type) + case type + when 'users/members' + last_export_members + when 'users/reservations' + last_export_reservations + when 'users/subscription' + last_export_subscriptions + else + raise TypeError "unknown export type: #{type}" + end + end + + private + + def last_export_subscriptions + Export.where(category: 'users', export_type: 'subscriptions') + .where('created_at > ?', Subscription.maximum('updated_at')) + .last + end + + def last_export_reservations + Export.where(category: 'users', export_type: 'reservations') + .where('created_at > ?', Reservation.maximum('updated_at')) + .last + end + + def last_export_members + last_update = [ + User.members.maximum('updated_at'), + Profile.where(user_id: User.members).maximum('updated_at'), + InvoicingProfile.where(user_id: User.members).maximum('updated_at'), + StatisticProfile.where(user_id: User.members).maximum('updated_at'), + Subscription.maximum('updated_at') || DateTime.current + ].max + + Export.where(category: 'users', export_type: 'members') + .where('created_at > ?', last_update) + .last + end + end +end diff --git a/app/services/members/members_service.rb b/app/services/members/members_service.rb index c1009d214..7e7bf8807 100644 --- a/app/services/members/members_service.rb +++ b/app/services/members/members_service.rb @@ -107,6 +107,44 @@ class Members::MembersService params end + def self.last_registered(limit) + query = User.active.with_role(:member) + .includes(:statistic_profile, profile: [:user_avatar]) + .where('is_allow_contact = true AND confirmed_at IS NOT NULL') + .order('created_at desc') + .limit(limit) + + # remove unmerged profiles from list + members = query.to_a + members.delete_if(&:need_completion?) + + [query, members] + end + + def update_role(new_role, new_group_id = Group.first.id) + # do nothing if the role does not change + return if new_role == @member.role + + # update role + ex_role = @member.role.to_sym + @member.remove_role ex_role + @member.add_role new_role + + # if the new role is 'admin', then change the group to the admins group, otherwise to change to the provided group + group_id = new_role == 'admin' ? Group.find_by(slug: 'admins').id : new_group_id + @member.update(group_id: group_id) + + # notify + NotificationCenter.call type: 'notify_user_role_update', + receiver: @member, + attached_object: @member + + NotificationCenter.call type: 'notify_admins_role_update', + receiver: User.admins_and_managers, + attached_object: @member, + meta_data: { ex_role: ex_role } + end + private def notify_user_profile_complete(previous_state) diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index d2687bec2..ab7c05d53 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -974,15 +974,20 @@ en: to_complete: "To complete" refuse_documents: "Refusing the documents" refuse_documents_info: "After verification, you may notify the member that the evidence submitted is not acceptable. You can specify the reasons for your refusal and indicate the actions to be taken. The member will be notified by e-mail." - #edit a member - members_edit: + change_role_modal: change_role: "Change role" - warning_role_change: "

Warning: changing the role of a user is not a harmless operation. Is not currently possible to dismiss a user to a lower privileged role.

  • Members can only book reservations for themselves, paying by card or wallet.
  • Managers can book reservations for themselves, paying by card or wallet, and for other members and managers, by collecting payments at the checkout.
  • Administrators can only book reservations for members and managers, by collecting payments at the checkout. Moreover, they can change every settings of the application.
" + warning_role_change: "

Warning: changing the role of a user is not a harmless operation.

  • Members can only book reservations for themselves, paying by card or wallet.
  • Managers can book reservations for themselves, paying by card or wallet, and for other members and managers, by collecting payments at the checkout.
  • Administrators can only book reservations for members and managers, by collecting payments at the checkout. Moreover, they can change every settings of the application.
" + new_role: "New role" admin: "Administrator" manager: "Manager" member: "Member" + new_group: "New group" + new_group_help: "Members and managers must be placed in a group." + confirm: "Change role" role_changed: "Role successfully changed from {OLD} to {NEW}." error_while_changing_role: "An error occurred while changing the role. Please try again later." + #edit a member + members_edit: subscription: "Subscription" duration: "Duration:" expires_at: "Expires at:" From 2b805161775bce28fd55cdcf77d65d18912e9edf Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 5 Oct 2022 13:11:17 +0200 Subject: [PATCH 16/17] (bug) unable to generate statistics This bug is due to the refactoring of the statistics builder service, in 2022 august. The default_options were not defined so the nightly worker has no luck to run. The statistics may not have been built since then, so a rebuild is required --- CHANGELOG.md | 2 ++ app/services/statistics/builder_service.rb | 10 ++++++++++ test/services/statistic_service_test.rb | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a570eb140..00250404c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog Fab-manager - Ability to dismiss a user to a lower privileged role +- Fix a bug: unable to generate statistics +- [TODO DEPLOY] `rails fablab:maintenance:regenerate_statistics[2022,08]` ## v5.4.20 2022 September 27 diff --git a/app/services/statistics/builder_service.rb b/app/services/statistics/builder_service.rb index 78e3a829a..53d7a49a5 100644 --- a/app/services/statistics/builder_service.rb +++ b/app/services/statistics/builder_service.rb @@ -12,5 +12,15 @@ class Statistics::BuilderService Statistics::Builders::MembersBuilderService.build(options) Statistics::Builders::ProjectsBuilderService.build(options) end + + private + + def default_options + yesterday = 1.day.ago + { + start_date: yesterday.beginning_of_day, + end_date: yesterday.end_of_day + } + end end end diff --git a/test/services/statistic_service_test.rb b/test/services/statistic_service_test.rb index db10ba4b8..ef1556fa1 100644 --- a/test/services/statistic_service_test.rb +++ b/test/services/statistic_service_test.rb @@ -9,6 +9,10 @@ class StatisticServiceTest < ActionDispatch::IntegrationTest login_as(@admin, scope: :user) end + test 'build default stats' do + ::Statistics::BuilderService.generate_statistic + end + test 'build stats' do # Create a reservation to generate an invoice machine = Machine.find(1) From 5d90451e3d7c1105da4d1e76ed2902f9a872a255 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 5 Oct 2022 13:24:17 +0200 Subject: [PATCH 17/17] Version 5.4.21 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00250404c..37fbe40b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +## v5.4.21 2022 October 05 + - Ability to dismiss a user to a lower privileged role - Fix a bug: unable to generate statistics - [TODO DEPLOY] `rails fablab:maintenance:regenerate_statistics[2022,08]` diff --git a/package.json b/package.json index 318be1130..73097400e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.20", + "version": "5.4.21", "description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.", "keywords": [ "fablab",