diff --git a/app/controllers/api/payments_controller.rb b/app/controllers/api/payments_controller.rb index 6732ebddb..2a65817f4 100644 --- a/app/controllers/api/payments_controller.rb +++ b/app/controllers/api/payments_controller.rb @@ -103,7 +103,10 @@ class API::PaymentsController < API::ApiController current_user.id end is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id) - .pay_and_save(@reservation, payment_details: details, payment_intent_id: intent.id) + .pay_and_save(@reservation, + payment_details: details, + payment_intent_id: intent.id, + schedule: params[:cart_items][:reservation][:payment_schedule]) if intent.class == Stripe::PaymentIntent Stripe::PaymentIntent.update( intent.id, diff --git a/app/controllers/api/reservations_controller.rb b/app/controllers/api/reservations_controller.rb index 549ee2a07..50f0ee6f4 100644 --- a/app/controllers/api/reservations_controller.rb +++ b/app/controllers/api/reservations_controller.rb @@ -35,7 +35,9 @@ class API::ReservationsController < API::ApiController @reservation = Reservation.new(reservation_params) is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id) - .pay_and_save(@reservation, payment_details: price[:price_details]) + .pay_and_save(@reservation, + payment_details: price[:price_details], + schedule: params[:reservation][:payment_schedule]) if is_reserve SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 9690ca756..9726a3766 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -120,7 +120,7 @@ class Reservation < ApplicationRecord pending_invoice_items.each(&:delete) end - def save_with_payment(operator_profile_id, payment_details, payment_intent_id = nil) + def save_with_payment(operator_profile_id, payment_details, payment_intent_id = nil, schedule: false) operator = InvoicingProfile.find(operator_profile_id)&.user method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe' @@ -138,7 +138,7 @@ class Reservation < ApplicationRecord if plan_id self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id) subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil } - if subscription.save_with_payment(operator_profile_id, invoice: false) + if subscription.save_with_payment(operator_profile_id, invoice: false, schedule: schedule) invoice.invoice_items.push InvoiceItem.new( amount: payment_details[:elements][:plan], description: subscription.plan.name, diff --git a/app/services/reservations/reserve.rb b/app/services/reservations/reserve.rb index d3d57587d..44f3a9fe3 100644 --- a/app/services/reservations/reserve.rb +++ b/app/services/reservations/reserve.rb @@ -10,8 +10,7 @@ class Reservations::Reserve end def pay_and_save(reservation, payment_details: nil, payment_intent_id: nil, schedule: false) - # TODO, pass the schedule payment up to subscription.save_with_payment(... schedule: schedule) reservation.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id - reservation.save_with_payment(operator_profile_id, payment_details, payment_intent_id) + reservation.save_with_payment(operator_profile_id, payment_details, payment_intent_id, schedule: schedule) end end diff --git a/app/workers/stripe_worker.rb b/app/workers/stripe_worker.rb index 14028554d..a44b63f78 100644 --- a/app/workers/stripe_worker.rb +++ b/app/workers/stripe_worker.rb @@ -53,7 +53,6 @@ class StripeWorker { name: object.name }, { api_key: Setting.get('stripe_secret_key') } ) - p.product else product = Stripe::Product.create( { diff --git a/db/structure.sql b/db/structure.sql index 3436e6be6..e5ce3175b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -108,8 +108,8 @@ SET default_tablespace = ''; CREATE TABLE public.abuses ( id integer NOT NULL, - signaled_id integer, signaled_type character varying, + signaled_id integer, first_name character varying, last_name character varying, email character varying, @@ -187,8 +187,8 @@ CREATE TABLE public.addresses ( locality character varying, country character varying, postal_code character varying, - placeable_id integer, placeable_type character varying, + placeable_id integer, created_at timestamp without time zone, updated_at timestamp without time zone ); @@ -263,8 +263,8 @@ CREATE TABLE public.ar_internal_metadata ( CREATE TABLE public.assets ( id integer NOT NULL, - viewable_id integer, viewable_type character varying, + viewable_id integer, attachment character varying, type character varying, created_at timestamp without time zone, @@ -504,8 +504,8 @@ ALTER SEQUENCE public.coupons_id_seq OWNED BY public.coupons.id; CREATE TABLE public.credits ( id integer NOT NULL, - creditable_id integer, creditable_type character varying, + creditable_id integer, plan_id integer, hours integer, created_at timestamp without time zone, @@ -1046,8 +1046,8 @@ ALTER SEQUENCE public.invoice_items_id_seq OWNED BY public.invoice_items.id; CREATE TABLE public.invoices ( id integer NOT NULL, - invoiced_id integer, invoiced_type character varying, + invoiced_id integer, stp_invoice_id character varying, total integer, created_at timestamp without time zone, @@ -1227,15 +1227,15 @@ ALTER SEQUENCE public.machines_id_seq OWNED BY public.machines.id; CREATE TABLE public.notifications ( id integer NOT NULL, receiver_id integer, - attached_object_id integer, attached_object_type character varying, + attached_object_id integer, notification_type_id integer, is_read boolean DEFAULT false, created_at timestamp without time zone, updated_at timestamp without time zone, receiver_type character varying, is_send boolean DEFAULT false, - meta_data jsonb DEFAULT '{}'::jsonb + meta_data jsonb DEFAULT '"{}"'::jsonb ); @@ -1656,8 +1656,8 @@ CREATE TABLE public.prices ( id integer NOT NULL, group_id integer, plan_id integer, - priceable_id integer, priceable_type character varying, + priceable_id integer, amount integer, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL @@ -1972,8 +1972,8 @@ CREATE TABLE public.reservations ( message text, created_at timestamp without time zone, updated_at timestamp without time zone, - reservable_id integer, reservable_type character varying, + reservable_id integer, nb_reserve_places integer, statistic_profile_id integer ); @@ -2005,8 +2005,8 @@ ALTER SEQUENCE public.reservations_id_seq OWNED BY public.reservations.id; CREATE TABLE public.roles ( id integer NOT NULL, name character varying, - resource_id integer, resource_type character varying, + resource_id integer, created_at timestamp without time zone, updated_at timestamp without time zone ); @@ -2942,8 +2942,8 @@ CREATE TABLE public.users_roles ( CREATE TABLE public.wallet_transactions ( id integer NOT NULL, wallet_id integer, - transactable_id integer, transactable_type character varying, + transactable_id integer, transaction_type character varying, amount integer, created_at timestamp without time zone NOT NULL, @@ -4032,6 +4032,14 @@ ALTER TABLE ONLY public.roles ADD CONSTRAINT roles_pkey PRIMARY KEY (id); +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + + -- -- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -5096,29 +5104,6 @@ CREATE INDEX profiles_lower_unaccent_last_name_trgm_idx ON public.profiles USING CREATE INDEX projects_search_vector_idx ON public.projects USING gin (search_vector); --- --- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING btree (version); - - --- --- Name: accounting_periods accounting_periods_del_protect; Type: RULE; Schema: public; Owner: - --- - -CREATE RULE accounting_periods_del_protect AS - ON DELETE TO public.accounting_periods DO INSTEAD NOTHING; - - --- --- Name: accounting_periods accounting_periods_upd_protect; Type: RULE; Schema: public; Owner: - --- - -CREATE RULE accounting_periods_upd_protect AS - ON UPDATE TO public.accounting_periods DO INSTEAD NOTHING; - - -- -- Name: projects projects_search_content_trigger; Type: TRIGGER; Schema: public; Owner: - -- @@ -5653,7 +5638,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20140605125131'), ('20140605142133'), ('20140605151442'), -('20140606133116'), ('20140609092700'), ('20140609092827'), ('20140610153123'), @@ -5722,14 +5706,12 @@ INSERT INTO "schema_migrations" (version) VALUES ('20150507075620'), ('20150512123546'), ('20150520132030'), -('20150520133409'), ('20150526130729'), ('20150527153312'), ('20150529113555'), ('20150601125944'), ('20150603104502'), ('20150603104658'), -('20150603133050'), ('20150604081757'), ('20150604131525'), ('20150608142234'), @@ -5811,7 +5793,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20160905142700'), ('20160906094739'), ('20160906094847'), -('20160906145713'), ('20160915105234'), ('20161123104604'), ('20170109085345'), diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 25add588e..be904051e 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -100,7 +100,7 @@ value_history_10: id: 10 setting_id: 10 invoicing_profile_id: 1 - value: YYMMmmmX[/VL]R[/A]S[/E] + value: YYMMmmmX[/VL]R[/A] created_at: 2018-12-17 11:23:01.603733000 Z updated_at: 2018-12-17 11:23:01.603733000 Z footprint: ed23a2eb1903befc977621bc3c3b19aad831fe550ebaa99e9299238b3d93c275 @@ -693,3 +693,11 @@ history_value_73: value: true created_at: 2020-06-17 10:48:19.002417000 Z updated_at: 2020-06-17 10:48:19.002417000 Z + +value_history_74: + id: 74 + setting_id: 10 + invoicing_profile_id: 1 + value: YYMMmmmX[/VL]R[/A]S[/E] + created_at: 2020-12-14 14:37:35.615124000 Z + updated_at: 2020-12-14 14:37:35.615124000 Z diff --git a/test/fixtures/machines.yml b/test/fixtures/machines.yml index 527ea6ca2..38a026d4e 100644 --- a/test/fixtures/machines.yml +++ b/test/fixtures/machines.yml @@ -22,6 +22,7 @@ machine_1: created_at: 2016-04-04 14:11:34.210242000 Z updated_at: 2016-04-04 14:11:34.210242000 Z slug: decoupeuse-laser + stp_product_id: prod_IZPyHpMCl38iQl machine_2: id: 2 @@ -38,6 +39,7 @@ machine_2: created_at: 2016-04-04 14:11:34.274025000 Z updated_at: 2016-04-04 14:11:34.274025000 Z slug: decoupeuse-vinyle + stp_product_id: prod_IZPyPShaaRgSML machine_3: id: 3 @@ -54,6 +56,7 @@ machine_3: created_at: 2016-04-04 14:11:34.304247000 Z updated_at: 2016-04-04 14:11:34.304247000 Z slug: shopbot-grande-fraiseuse + stp_product_id: prod_IZPyEjmdfMowhY machine_4: id: 4 @@ -67,6 +70,7 @@ machine_4: created_at: 2001-01-01 14:11:34.341810000 Z updated_at: 2001-01-01 14:11:34.341810000 Z slug: imprimante-3d + stp_product_id: prod_IZPy85vZOQpAo5 machine_5: id: 5 @@ -89,6 +93,7 @@ machine_5: created_at: 2016-04-04 14:11:34.379481000 Z updated_at: 2016-04-04 14:11:34.379481000 Z slug: petite-fraiseuse + stp_product_id: prod_IZPyBJEgbcpWMC machine_6: id: 6 @@ -123,3 +128,4 @@ machine_6: created_at: 2016-04-04 14:11:34.424740000 Z updated_at: 2016-04-04 14:11:34.424740000 Z slug: form1-imprimante-3d + stp_product_id: prod_IZPyjCzvLmLWAz diff --git a/test/fixtures/plans.yml b/test/fixtures/plans.yml index af996e226..834e07b46 100644 --- a/test/fixtures/plans.yml +++ b/test/fixtures/plans.yml @@ -15,6 +15,8 @@ plan_1: base_name: Mensuel ui_weight: 1 interval_count: 1 + slug: mensuel + stp_product_id: prod_IZPyXhfyNiGkWR plan_2: id: 2 @@ -32,6 +34,8 @@ plan_2: base_name: Sleede ui_weight: 5 interval_count: 2 + slug: sleede + stp_product_id: prod_IZPykam7a4satn plan_3: id: 3 @@ -49,4 +53,26 @@ plan_3: type: Plan base_name: Mensuel tarif réduit ui_weight: 0 + interval_count: 1* + slug: mensuel-tarif-reduit + stp_product_id: prod_IZPyM4N36h86G0 + +plan_schedulable: + id: 4 + name: Abonnement mensualisable - standard, association, year + amount: 113600 + interval: year + group_id: 1 + stp_plan_id: + created_at: 2020-12-14 14:10:11.056241000 Z + updated_at: 2020-12-14 14:10:11.137421000 Z + training_credit_nb: 1 + is_rolling: true + description: + type: Plan + base_name: Abonnement mensualisable + ui_weight: 10 interval_count: 1 + monthly_payment: true + slug: abonnement-mensualisable + stp_product_id: prod_IZQAhb9nLu4jfN diff --git a/test/fixtures/spaces.yml b/test/fixtures/spaces.yml index 242041286..8a50e82cc 100644 --- a/test/fixtures/spaces.yml +++ b/test/fixtures/spaces.yml @@ -7,3 +7,4 @@ space_1: created_at: 2017-02-15 15:55:04.123928000 Z updated_at: 2017-02-15 15:55:04.123928000 Z characteristics: Scie à chantourner, rabot, dégauchisseuse, chanfreineuse et pyrograveur + stp_product_id: prod_IZPyHjIb2owoB8 diff --git a/test/fixtures/trainings.yml b/test/fixtures/trainings.yml index a25c18e82..24d9521bd 100644 --- a/test/fixtures/trainings.yml +++ b/test/fixtures/trainings.yml @@ -7,6 +7,7 @@ training_1: nb_total_places: slug: formation-imprimante-3d description: + stp_product_id: prod_IZPyXw6BDBBFOg training_2: id: 2 @@ -16,6 +17,7 @@ training_2: nb_total_places: slug: formation-laser-vinyle description: + stp_product_id: prod_IZPytTl1wSB5jH training_3: id: 3 @@ -25,6 +27,7 @@ training_3: nb_total_places: slug: formation-petite-fraiseuse-numerique description: + stp_product_id: prod_IZPyAA1A4QfEyL training_4: id: 4 @@ -34,6 +37,7 @@ training_4: nb_total_places: slug: formation-shopbot-grande-fraiseuse description: + stp_product_id: prod_IZPyU27NjDSmqB training_5: id: 5 @@ -43,3 +47,4 @@ training_5: nb_total_places: slug: formation-logiciel-2d description: + stp_product_id: prod_IZPyvdgQHMByB3 diff --git a/test/integration/subscriptions/create_as_user_test.rb b/test/integration/subscriptions/create_as_user_test.rb index 74e83e6d6..8adcb7234 100644 --- a/test/integration/subscriptions/create_as_user_test.rb +++ b/test/integration/subscriptions/create_as_user_test.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'test_helper' + class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest setup do @user = User.find_by(username: 'jdupond') @@ -166,4 +168,48 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest assert_equal invoice.wallet_amount / 100.0, transaction.amount assert_equal invoice.wallet_transaction_id, transaction.id end + + test 'user takes a subscription with payment schedule' do + plan = Plan.find_by(group_id: @user.group.id, type: 'Plan', base_name: 'Abonnement mensualisable') + + VCR.use_cassette('subscriptions_user_setup_intent') do + get "/api/payments/setup_intent/#{@user.id}" + end + + # Check response format & status + assert_equal 200, response.status, response.body + assert_equal Mime[:json], response.content_type + + # Check the correct object was signaled + setup_intent = json_response(response.body) + + VCR.use_cassette('subscriptions_user_create_with_payment_schedule') do + post '/api/payments/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + subscription: { + plan_id: plan.id, + payment_schedule: true, + payment_method: 'stripe' + } + }, + setup_intent_id: setup_intent[:client_secret] + }.to_json, headers: default_headers + end + + # Check response format & status + assert_equal 201, response.status, response.body + assert_equal Mime[:json], response.content_type + + # Check the correct plan was subscribed + subscription = json_response(response.body) + assert_equal plan.id, subscription[:plan_id], 'subscribed plan does not match' + + # Check that the user has the correct subscription + assert_not_nil @user.subscription, "user's subscription was not found" + assert_not_nil @user.subscription.plan, "user's subscribed plan was not found" + assert_equal plan.id, @user.subscription.plan_id, "user's plan does not match" + + end end