diff --git a/Gemfile b/Gemfile index c58581e..ab278e0 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ ruby '2.4.1' gemspec gem 'faker' +gem 'stripe_event' gem 'refile', require: 'refile/rails', git: 'https://github.com/manfe/refile.git' gem 'elasticsearch-model', git: 'https://github.com/elasticsearch/elasticsearch-rails.git' @@ -15,7 +16,7 @@ gem 'elasticsearch-rails', git: 'https://github.com/elasticsearch/elasticsearch- gem 'sidekiq' gem 'aws-sdk' gem 'doorkeeper', '4.3.2' -gem 'redis-objects' +gem 'stripe' group :development, :test do gem 'byebug', platform: :mri @@ -27,6 +28,10 @@ group :development, :test do gem 'awesome_print' end +group :development do + gem 'ultrahook' +end + group :test do gem 'database_cleaner' gem 'poltergeist' diff --git a/Gemfile.lock b/Gemfile.lock index c7cf4f0..98465e1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -80,11 +80,11 @@ PATH rails rails-jquery-autocomplete redis (< 4) - redis-objects refile rmagick sidekiq slim-rails + stripe versionist whenever @@ -114,7 +114,7 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_model_serializers (0.10.7) + active_model_serializers (0.10.9) actionpack (>= 4.1, < 6) activemodel (>= 4.1, < 6) case_transform (>= 0.2) @@ -850,6 +850,7 @@ GEM thor (>= 0.14, < 2.0) jquery-ui-rails (6.0.1) railties (>= 3.2.16) + json (2.0.2) jsonapi-renderer (0.2.0) kaminari (1.1.1) activesupport (>= 4.1.0) @@ -878,12 +879,14 @@ GEM multi_json (1.13.1) multipart-post (2.0.0) mustermann (1.0.2) + net-http-persistent (3.0.0) + connection_pool (~> 2.2) netrc (0.11.0) nio4r (2.3.1) nokogiri (1.8.4) mini_portile2 (~> 2.3.0) - omniauth (1.8.1) - hashie (>= 3.4.6, < 3.6.0) + omniauth (1.9.0) + hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) paper_trail (9.2.0) activerecord (>= 4.2, < 5.3) @@ -902,7 +905,7 @@ GEM activerecord (>= 3.0) powerpack (0.1.2) public_suffix (3.0.2) - pundit (2.0.0) + pundit (2.0.1) activesupport (>= 3.0.0) rack (2.0.5) rack-cors (1.0.2) @@ -927,7 +930,7 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - rails-jquery-autocomplete (1.0.4) + rails-jquery-autocomplete (1.0.5) rails (>= 3.2) railties (5.1.6) actionpack (= 5.1.6) @@ -947,8 +950,6 @@ GEM rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) redis (3.3.5) - redis-objects (1.3.1) - redis (~> 3.3) request_store (1.4.1) rack (>= 1.4) responders (2.4.0) @@ -958,7 +959,7 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rmagick (2.16.0) + rmagick (3.0.0) rspec-core (3.8.0) rspec-support (~> 3.8.0) rspec-expectations (3.8.1) @@ -1011,13 +1012,13 @@ GEM rack (~> 2.0) rack-protection (= 2.0.3) tilt (~> 2.0) - slim (3.0.9) + slim (4.0.1) temple (>= 0.7.6, < 0.9) - tilt (>= 1.3.3, < 2.1) - slim-rails (3.1.3) + tilt (>= 2.0.6, < 2.1) + slim-rails (3.2.0) actionpack (>= 3.1) railties (>= 3.1) - slim (~> 3.0) + slim (>= 3.0, < 5.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -1025,12 +1026,20 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - temple (0.8.0) + stripe (4.9.0) + faraday (~> 0.13) + net-http-persistent (~> 3.0) + stripe_event (2.3.0) + activesupport (>= 3.1) + stripe (>= 2.8, < 6) + temple (0.8.1) thor (0.20.0) thread_safe (0.3.6) tilt (2.0.8) tzinfo (1.2.5) thread_safe (~> 0.1) + ultrahook (0.1.5) + json (>= 1.8.0) unf (0.1.4) unf_ext unf_ext (0.0.7.5) @@ -1050,7 +1059,7 @@ GEM chronic (>= 0.6.3) xpath (3.1.0) nokogiri (~> 1.8) - yard (0.9.16) + yard (0.9.24) PLATFORMS ruby @@ -1074,7 +1083,6 @@ DEPENDENCIES pg (~> 0.18) poltergeist rack-cors - redis-objects refile! rspec-rails rubocop @@ -1082,10 +1090,13 @@ DEPENDENCIES rubocop-rspec shoulda-matchers! sidekiq + stripe + stripe_event + ultrahook webmock RUBY VERSION ruby 2.4.1p111 BUNDLED WITH - 1.16.6 + 1.17.3 diff --git a/app/assets/javascripts/active_admin.js.coffee b/app/assets/javascripts/active_admin.js.coffee index 5b41ba8..0d4d87d 100644 --- a/app/assets/javascripts/active_admin.js.coffee +++ b/app/assets/javascripts/active_admin.js.coffee @@ -5,6 +5,8 @@ #= require autocomplete-rails #= require active_admin/achievement #= require active_admin/reward +#= require active_admin/subscription + $ -> COUNT = -1; diff --git a/app/assets/javascripts/active_admin/subscription.coffee b/app/assets/javascripts/active_admin/subscription.coffee new file mode 100644 index 0000000..0c24802 --- /dev/null +++ b/app/assets/javascripts/active_admin/subscription.coffee @@ -0,0 +1,14 @@ +$ -> + $('#owner_user_input').parent().hide() + $('#owner_organisation_input').parent().hide() + + $('#subscription_owner_type').on 'change', (e)-> + console.log $(e.target).val() + + if $(e.target).val() == 'BeachApiCore::User' + $('#owner_user_input').parent().show() + $('#owner_organisation_input').parent().hide() + + if $(e.target).val() == 'BeachApiCore::Organisation' + $('#owner_user_input').parent().hide() + $('#owner_organisation_input').parent().show() \ No newline at end of file diff --git a/app/controllers/beach_api_core/concerns/v1/apipie_concern.rb b/app/controllers/beach_api_core/concerns/v1/apipie_concern.rb index ab04098..9e5d6b8 100644 --- a/app/controllers/beach_api_core/concerns/v1/apipie_concern.rb +++ b/app/controllers/beach_api_core/concerns/v1/apipie_concern.rb @@ -13,6 +13,16 @@ def apipie_user @_apipie_user end + def apipie_plan + @_apipie_subscription_plan ||= BeachApiCore::Plan.new(id: fake_id, + name: Faker::Name.title, + stripe_id: Faker::Lorem.word, + amount: Faker::Number.between(1000, 10000), + interval: %w(day month year).sample, + plan_for: %w(organisation user).sample + ) + end + def apipie_asset return @_apipie_asset if @_apipie_asset @_apipie_asset = BeachApiCore::Asset.new(id: fake_id, diff --git a/app/controllers/beach_api_core/concerns/v1/apipie_response_concern.rb b/app/controllers/beach_api_core/concerns/v1/apipie_response_concern.rb index c1eb085..a3a65da 100644 --- a/app/controllers/beach_api_core/concerns/v1/apipie_response_concern.rb +++ b/app/controllers/beach_api_core/concerns/v1/apipie_response_concern.rb @@ -9,6 +9,10 @@ def apipie_user_response ).as_json end + def apipie_plan_response + pretty BeachApiCore::PlanSerializer.new(apipie_plan) + end + def apipie_organisation_user_response pretty BeachApiCore::OrganisationUserSerializer.new( apipie_user, current_organisation: apipie_organisation diff --git a/app/controllers/beach_api_core/v1/organisations_controller.rb b/app/controllers/beach_api_core/v1/organisations_controller.rb index 6a1c372..3a59333 100644 --- a/app/controllers/beach_api_core/v1/organisations_controller.rb +++ b/app/controllers/beach_api_core/v1/organisations_controller.rb @@ -94,7 +94,7 @@ def get_current private def organisation_params - params.require(:organisation).permit(:name, logo_properties: logo_params, logo_image_attributes: %i(file base64)) + params.require(:organisation).permit(:name, :email, logo_properties: logo_params, logo_image_attributes: %i(file base64)) end def logo_params @@ -109,7 +109,7 @@ def logo_params def users_by_roles(users) return users unless params[:roles].present? filtered_users = users.joining { assignments }.where.has do |u| - (u.assignments.role_id.in params[:roles]) & (u.assignments.keeper_id == current_organisation.id) & + (u.assignments.keeper_id == current_organisation.id) & ( u.assignments.keeper_type == 'BeachApiCore::Organisation') end filtered_users.any? ? filtered_users : users diff --git a/app/controllers/beach_api_core/v1/plans_controller.rb b/app/controllers/beach_api_core/v1/plans_controller.rb new file mode 100644 index 0000000..3688b06 --- /dev/null +++ b/app/controllers/beach_api_core/v1/plans_controller.rb @@ -0,0 +1,65 @@ +module BeachApiCore + class V1::PlansController < BeachApiCore::V1::BaseController + include PlansDoc + include BeachApiCore::Concerns::V1::ResourceConcern + + before_action :doorkeeper_authorize! + + resource_description do + name I18n.t('api.resource_description.resources.plans') + end + + def index + render_json_success(Plan.all, :ok, + each_serializer: PlanSerializer, + root: :plans) + end + + def create + if admin + result = PlanCreate.call(params: plan_params) + if result.success? + render_json_success(result.plan, :ok, serializer: BeachApiCore::PlanSerializer, root: :plan) + else + render_json_error({ message: result.message }, result.status) + end + else + render_json_error({message: "Wrong permissions"}) + end + end + + def show + plan = Plan.find_by(id: params[:id]) + if plan.nil? + render_json_error({message: "There are no such plan"}) + else + render_json_success(plan) + end + end + + def destroy + if admin + plan = Plan.find_by(id: params[:id]) + if plan.nil? + render_json_error({message: "Could not remove plan"}) + else + begin + if plan.destroy + head :no_content + else + render_json_error({message: "Could not remove plan"}) + end + rescue => e + render_json_error({message: "Could not remove plan"}) + end + end + end + end + + private + + def plan_params + params.require(:plan).permit(:name, :amount, :interval, :stripe_id, :plan_for, :currency, :trial_period_days, :amount_per_additional_user, :users_count) + end + end +end \ No newline at end of file diff --git a/app/controllers/beach_api_core/v1/subscriptions_controller.rb b/app/controllers/beach_api_core/v1/subscriptions_controller.rb new file mode 100644 index 0000000..e73e05a --- /dev/null +++ b/app/controllers/beach_api_core/v1/subscriptions_controller.rb @@ -0,0 +1,176 @@ +module BeachApiCore + class V1::SubscriptionsController < BeachApiCore::V1::BaseController + include SubscriptionsDoc + + before_action :doorkeeper_authorize! + skip_before_action :doorkeeper_authorize!, only: :invoice_created + + resource_description do + name I18n.t('api.resource_description.resources.subscriptions') + end + + def create + unless subscription_params[:owner_id].nil? + keeper = subscription_params[:subscription_for] == "user" ? current_user : BeachApiCore::Organisation.find_by(:id => subscription_params[:owner_id]) + end + if keeper.nil? + render_json_error({message: "Failed subscription creation"}) + else + params[:subscription].delete(:subscription_for) + params[:subscription].delete(:organisation_id) + result = SubscriptionCreate.call(params: subscription_params, + owner: keeper) + if result.success? + render_json_success(result.subscription,:ok, serializer: BeachApiCore::SubscriptionSerializer, root: :subscription) + else + render_json_error({ message: result.message }, result.status) + end + end + end + + def update + subs = BeachApiCore::Subscription.find_by(id: params[:id]) + if subs.nil? + render_json_error({message: "Not Found"}) + else + if admin || current_user.subscription.id == subs.id + result = BeachApiCore::SubscriptionUpdate.call(:subscription => subs, :params => subscription_update_params) + keeper = subs.keeper_type=="BeachApiCore::Organisation" ? BeachApiCore::Organisation.find_by(:id => subs.keeper_id) : BeachApiCore::User.find_by(:id => subs.keeper_id) + keeper.invoices.create(:invoice_url_link => invoice.hosted_invoice_url, :invoice_pdf_link => invoice.invoice_pdf) + if result.success? + render_json_success(result.subscription, result.status, serializer: BeachApiCore::SubscriptionSerializer, root: :subscription) + else + render_json_error({ message: result.message }, result.status) + end + else + render_json_error({message: "Wrong Access"}) + end + end + end + + + def show + subs = BeachApiCore::Subscription.find_by(id: params[:id]) + if subs.nil? + render_json_error({message: "Not Found"}) + else + if admin || current_user.subscription.id == subs.id + render_json_success(subs,:ok, serializer: BeachApiCore::SubscriptionSerializer, root: :subscription) + else + render_json_error({message: "Wrong Access"}) + end + end + + end + + def destroy + subs = BeachApiCore::Subscription.find_by(id: params[:id]) + if subs.nil? + render_json_error({message: "Not Found"}) + else + if admin || current_user.subscription.id == subs.id + if subs.destroy + head :no_content + else + render_json_error({message: "Can't delete subscription"}) + end + else + render_json_error({message: "Wrong Access"}) + end + end + end + + def create_customer + Stripe.api_key = ENV['STRIPE_SECRET_KEY'] + client = subscription_params[:subscription_for]=="user" ? current_user : BeachApiCore::Organisation.find_by(:id => subscription_params[:owner_id]) + unless client.nil? + card_token = Stripe::Token.create( + { + card: { + number: card_params[:number], + exp_month: card_params[:exp_month], + exp_year: card_params[:exp_year], + cvc: card_params[:cvc] + } + } + ) + customer = Stripe::Customer.create(email: client.email, card: card_token.id) + client.update_attribute(:stripe_customer_token, customer.id) + render_json_success(:message => "Customer created successfully") + else + render_json_error(:message => "User or organisation not found") + end + rescue Stripe::CardError => e + render_json_error({:message => "Wrong card"}) + end + + def show_invoices + owner_type = params[:user_id].present? ? "BeachApiCore::User" : "BeachApiCore::Organisation" + keeper = owner_type=="BeachApiCore::User" ? BeachApiCore::User.find_by(:id => params[:user_id]) : BeachApiCore::Organisation.find_by(:id => params[:organisation_id]) + if owner_type=="BeachApiCore::User" && keeper.id == current_user.id + subs = BeachApiCore::Subscription.find_by(id: params[:id], owner_type: owner_type, :owner_id => keeper.id) + elsif owner_type=="BeachApiCore::Organisation" && keeper.organisations.where(:id => params[:organisation_id]).include?(current_user) + subs = BeachApiCore::Subscription.find_by(id: params[:id], owner_type: owner_type, :owner_id => keeper.id) + end + unless subs.nil? + invoices = BeachApiCore::Invoice.where(subscription_id: subs.id) + if invoices.empty? + render_json_error(message: "Invoices not found") + else + render_json_success(invoices) + end + else + render_json_error(message: "Wrong subscription") + end + rescue + render_json_error(message: "Wrong request") + end + + def update_quantity + unless params[:id].nil? + subs = BeachApiCore::Subscription.find_by(:id => params[:id]) + stripe_subs = Stripe::Subscription.retrieve(subs.stripe_subscription_id) + unless subs.nil? || stripe_subs.nil? + quantity = subs.get_quantity + unless stripe_subs.items.data[0].quantity == quantity + not_owner = !BeachApiCore::Membership.pluck(:owner, :group_type, :group_id, :member_type, :member_id).include?([true,subs.owner_type,subs.owner_id,"BeachApiCore::User",current_user.id]) + not_admin = !BeachApiCore::Assignment.pluck(:user_id ,:role_id ,:keeper_id ,:keeper_type).include?([current_user.id,1,subs.owner_id,subs.owner_type]) + unless not_owner || not_admin + Stripe::SubscriptionItem.update(stripe_subs.items.data[0].id, {plan: stripe_subs.items.data[0].plan.id, quantity: quantity}) + subs.create_invoice + render_json_success(:message => "Subscription updated successfully") + else + render_json_error(:message => "Wrong access") + end + else + render_json_error(:message => "Quantity not changed") + end + else + render_json_error(:message => "Wrong subscription") + end + else + render_json_error({:message => "Wrong id"}) + end + rescue + render_json_error(message: "Wrong request") + end + + private + + def subscription_params + params.require(:subscription).permit(:plan_id, :subscription_for, :owner_id) + end + + def subscription_update_params + params.require(:subscription).permit(:plan_id) + end + + def card_params + params.require(:card_params).permit(:number, :exp_month, :exp_year, :cvc) + end + + def have_subscription + !current_user.plan.nil? + end + end +end \ No newline at end of file diff --git a/app/docs/organisations_doc.rb b/app/docs/organisations_doc.rb index b899fb0..216346d 100644 --- a/app/docs/organisations_doc.rb +++ b/app/docs/organisations_doc.rb @@ -35,7 +35,15 @@ def index; end def published_applications; end api :POST, '/organisations', I18n.t('api.resource_description.descriptions.organisations.create') - param_group :organisation + param :organisation, Hash, required: true do + param :name, String, required: true + param :email, String, required: true, desc: "Organisation's email" + param :logo_properties, Hash, required: false + param :logo_image_attributes, Hash, required: false do + param :file, File, required: false, desc: "Postfield file" + param :base64, String, required: false, desc: "Encoded Base64 string" + end + end example "\"organisation\": #{apipie_organisation_response} \n#{I18n.t('api.resource_description.fail', description: I18n.t('api.resource_description.fails.errors_description'))}" @@ -77,4 +85,21 @@ def current; end header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true example "\"organisation\": #{apipie_organisation_response}" def get_current; end + + api :GET, 'organisation/:organisation_id/subscription/:id/show_invoices', 'Show invoices subscriptions of organisation' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + example "[ + { + \"id\": 12, + \"keeper_type\": \"BeachApiCore::Organisation\", + \"keeper_id\": 8, + \"subscription_id\": 14, + \"invoice_url_link\": \"https://pay.stripe.com/invoice/invst_yHsTACRTbcbf1h1eWvhRR\", + \"invoice_pdf_link\": \"https://pay.stripe.com/invoice/invst_yHsTACRTbcbf1h1eWvhRR/pdf\", + \"created_at\": \"2019-12-16T08:22:18.185Z\", + \"updated_at\": \"2019-12-16T08:22:18.185Z\" + }, + {...} +]" + def show_invoices; end end diff --git a/app/docs/plans_doc.rb b/app/docs/plans_doc.rb new file mode 100644 index 0000000..0f21948 --- /dev/null +++ b/app/docs/plans_doc.rb @@ -0,0 +1,56 @@ +module PlansDoc + extend Apipie::DSL::Concern + extend BeachApiCore::Concerns::V1::ApipieResponseConcern + + api :GET, '/plans', 'List all plans' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + example "\"plans\": [#{apipie_plan_response}, ...]" + def index; end + + api :GET, '/plans/:id', 'Show plan' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + example "\"plan\": #{apipie_plan_response}" + def show; end + + api :POST, '/plans', 'Create a new plan' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + param :plan, Hash, required: true do + param :name, String, required: true, desc: 'Human readable name of a plan' + param :amount, Integer, required: true + param :interval, %w(day month year), required: true, desc: 'Period for plan: day, month or year' + param :stripe_id, String, required: true, desc: 'Unique plan name for stripe' + param :plan_for, %w(organisation user), required: true, desc: 'Choose plan for keeper type: user or organisation' + param :trial_period_days, Integer, required: false, desc: 'Trial period at days' + param :billing_scheme, %w(tiered), required: false, desc: 'Required if plan_for \'organiastion\': Scheme for plan' + param :tiers, Array, required: false, desc: 'Required if billing_scheme = "tiered" :: Add tiers for plan' + param :tiers_mode, %w(graduated),required: false, desc: 'Required if billing_scheme = "tiered" :: Add mode of tiers for plan' + param :currency, String, required: true, desc: 'Add currency for plan' + end + example "{ + \"plan\":{ + \"amount\": 1999, + \"name\": \"Pllan for organisations\", + \"interval\": \"month\", + \"stripe_id\": \"Pllan_for_organisation\", + \"plan_for\": \"organisation\", + \"billing_scheme\": \"tiered\", + \"tiers\": [{\"unit_amount\":0, \"up_to\":5, \"flat_amount\": 700},{\"unit_amount\":300, \"up_to\":\"inf\"}], + \"users_count\": 10, + \"amount_per_additional_user\": 100, + \"trial_period_days\": 3, + \"currency\": \"usd\", + \"tiers_mode\": \"graduated\" + } + } + \"plan\": #{apipie_plan_response}" + def create; end + + api :DELETE, '/plans/:id', "Remove plan" + header 'Authorization', 'Bearer access_token', required: true + example '{ + "error": { + "message": "Could not remove plan" + } +}' + def destroy; end +end \ No newline at end of file diff --git a/app/docs/subscriptions_doc.rb b/app/docs/subscriptions_doc.rb new file mode 100644 index 0000000..683fddd --- /dev/null +++ b/app/docs/subscriptions_doc.rb @@ -0,0 +1,88 @@ +module SubscriptionsDoc + extend Apipie::DSL::Concern + extend BeachApiCore::Concerns::V1::ApipieResponseConcern + + api :POST, '/subscriptions', 'Create a subscription' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + param :subscription, Hash, required: true do + param :plan_id, Integer, required: true + param :subscription_for, %w(organisation user), required: true + param :organisation_id, Integer, required: false, desc: "Required only in case of subscription for organisation" + end + example ' + { + "subscription": { + "id": 5, + "subscription_for": "user", + "plan": { + "id": 6, + "stripe_id": "test1", + "name": "Test Stripe", + "amount": 2311, + "interval": "month", + "plan_for": "user" + } + } + }' + def create; end + + api :POST, '/subscriptions/create_customer', 'create_customer' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + param :card_params, Hash, required: true do + param :number, Integer, required: true + param :cvc, Integer, required: true + param :exp_month, Integer, required: true + param :exp_year, Integer, required: true + end + example '"message": "Customer created successfully"' + def create_customer; end + + api :GET, '/subscriptions/:id', 'Show subscription' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + example ' + { + "subscription": { + "id": 4, + "subscription_for": "user", + "plan": { + "id": 6, + "stripe_id": "test1", + "name": "Test Stripe", + "amount": 2311, + "interval": "month", + "plan_for": "user" + } + } + }' + def show; end + + api :PUT, '/subscriptions/:id', 'Update Subscription plan' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + param :subscription, Hash, required: true do + param :plan_id, Integer, required: true + end + example ' + { + "subscription": { + "id": 6, + "subscription_for": "user", + "plan": { + "id": 6, + "stripe_id": "test1", + "name": "Test Stripe", + "amount": 2311, + "interval": "month", + "plan_for": "user" + } + } + }' + def update; end + + api :DELETE, '/subscriptions/:id', "Cancel Subscription" + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + example '{ + "error": { + "message": "Can\'t delete subscription" + } +}' +end \ No newline at end of file diff --git a/app/docs/users_doc.rb b/app/docs/users_doc.rb index 6741bcf..9cac792 100644 --- a/app/docs/users_doc.rb +++ b/app/docs/users_doc.rb @@ -61,4 +61,21 @@ def update; end param :confirmation_token, String, required: true example "\"user\": #{apipie_user_response}" def confirm; end + + api :GET, 'users/:user_id/subscription/:id/show_invoices', 'Show invoices subscriptions of organisation' + header 'HTTP_AUTHORIZATION', 'Bearer access_token', required: true + example "[ + { + \"id\": 5, + \"keeper_type\": \"BeachApiCore::Organisation\", + \"keeper_id\": 12, + \"subscription_id\": 10, + \"invoice_url_link\": \"https://pay.stripe.com/invoice/invst_yHsTACRTbcbf1h1awdWRTasdz31zs\", + \"invoice_pdf_link\": \"https://pay.stripe.com/invoice/invst_yHsTACRTbcbf1h1awdWRTasdz31zs/pdf\", + \"created_at\": \"2019-12-16T08:22:18.185Z\", + \"updated_at\": \"2019-12-16T08:22:18.185Z\" + }, + {...} +]" + def show_invoices; end end diff --git a/app/interactors/beach_api_core/plan_create.rb b/app/interactors/beach_api_core/plan_create.rb new file mode 100644 index 0000000..914706c --- /dev/null +++ b/app/interactors/beach_api_core/plan_create.rb @@ -0,0 +1,15 @@ +module BeachApiCore + class PlanCreate + include Interactor + + def call + context.plan = Plan.new context.params + if context.plan.save + context.status = :created + else + context.status = :bad_request + context.fail! message: context.plan.errors.full_messages + end + end + end +end \ No newline at end of file diff --git a/app/interactors/beach_api_core/subscription_create.rb b/app/interactors/beach_api_core/subscription_create.rb new file mode 100644 index 0000000..373c4ec --- /dev/null +++ b/app/interactors/beach_api_core/subscription_create.rb @@ -0,0 +1,17 @@ +module BeachApiCore + class SubscriptionCreate + include Interactor + + def call + context.subscription = Subscription.new context.params + context.subscription.owner = context.owner + if context.subscription.save + BeachApiCore::Invoice.last.update_attribute(:subscription_id, context.subscription.id) + context.status = :created + else + context.status = :bad_request + context.fail! message: context.subscription.errors.full_messages + end + end + end +end \ No newline at end of file diff --git a/app/interactors/beach_api_core/subscription_update.rb b/app/interactors/beach_api_core/subscription_update.rb new file mode 100644 index 0000000..a4d53c5 --- /dev/null +++ b/app/interactors/beach_api_core/subscription_update.rb @@ -0,0 +1,12 @@ +class BeachApiCore::SubscriptionUpdate + include Interactor + + def call + if context.subscription.update context.params + context.status = :ok + else + context.status = :bad_request + context.fail! message: context.subscription.errors.full_messages + end + end +end diff --git a/app/models/beach_api_core/invoice.rb b/app/models/beach_api_core/invoice.rb new file mode 100644 index 0000000..7dd962a --- /dev/null +++ b/app/models/beach_api_core/invoice.rb @@ -0,0 +1,8 @@ +module BeachApiCore + class Invoice < ApplicationRecord + belongs_to :user, class_name: "BeachApiCore::User" + belongs_to :organisation, class_name: "BeachApiCore::Organisation" + + validates :invoice_url_link, :invoice_pdf_link, presence: true + end +end diff --git a/app/models/beach_api_core/membership.rb b/app/models/beach_api_core/membership.rb index e12ce64..6ab2b65 100644 --- a/app/models/beach_api_core/membership.rb +++ b/app/models/beach_api_core/membership.rb @@ -5,5 +5,21 @@ class Membership < ApplicationRecord belongs_to :member, polymorphic: true belongs_to :group, polymorphic: true + after_create :set_quantity + before_destroy :delete_quantity + + def set_quantity + if self.group_type == "BeachApiCore::Organisation" && !self.group.subscription.nil? + subscription = Stripe::Subscription.retrieve(BeachApiCore::Subscription.find_by(:owner_id => self.group_id, :owner_type => self.group_type).stripe_subscription_id) + Stripe::SubscriptionItem.update(subscription.items.data[0].id,{plan: subscription.items.data[0].plan.id, quantity: subscription.items.data[0].quantity+1}) + end + end + + def delete_quantity + if self.group_type == "BeachApiCore::Organisation" && !self.group.subscription.nil? + subscription = Stripe::Subscription.retrieve(BeachApiCore::Subscription.find_by(:owner_id => self.group_id, :owner_type => self.group_type).stripe_subscription_id) + Stripe::SubscriptionItem.update(subscription.items.data[0].id,{plan: subscription.items.data[0].plan.id, quantity: subscription.items.data[0].quantity-1}) + end + end end end diff --git a/app/models/beach_api_core/organisation.rb b/app/models/beach_api_core/organisation.rb index 15596b4..dcf2f4a 100644 --- a/app/models/beach_api_core/organisation.rb +++ b/app/models/beach_api_core/organisation.rb @@ -3,10 +3,12 @@ class Organisation < ApplicationRecord include BeachApiCore::Concerns::AssetConcern include BeachApiCore::Concerns::GenerateImageConcern validates :name, :application, presence: true + validate :check_on_owner_change, on: [:create, :update] belongs_to :application, class_name: 'Doorkeeper::Application' has_many :memberships, as: :group, inverse_of: :group, :dependent => :destroy has_many :users, through: :memberships, source: :member, source_type: 'BeachApiCore::User' + has_many :invoices, class_name: "BeachApiCore::Invoice", as: :keeper, dependent: :destroy has_many :applications, as: :publisher, class_name: 'Doorkeeper::Application' has_many :teams, through: :memberships, source: :member, source_type: 'BeachApiCore::Team' has_many :invitations, as: :group, inverse_of: :group @@ -16,7 +18,9 @@ class Organisation < ApplicationRecord has_one :organisation_plan, dependent: :destroy has_one :plan, through: :organisation_plan has_one :logo_image, class_name: 'BeachApiCore::Asset', as: :entity, inverse_of: :entity, dependent: :destroy - + has_one :subscription, class_name: "BeachApiCore::Subscription", as: :owner, dependent: :destroy + + accepts_nested_attributes_for :subscription accepts_nested_attributes_for :logo_image, allow_destroy: true, reject_if: :file_blank? accepts_nested_attributes_for :organisation_plan, allow_destroy: true, reject_if: proc { |attr| attr[:plan_id].blank? } @@ -31,5 +35,10 @@ def owners def generate_image? logo_image.blank? || ((saved_change_to_name? || saved_change_to_logo_properties?) && logo_image.generated?) end + + def check_on_owner_change + self.errors.add :subscription_owner, 'can\'t be changed while you have active subscription' unless self.subscription.nil? + end + end end diff --git a/app/models/beach_api_core/plan.rb b/app/models/beach_api_core/plan.rb index ccaad34..99236b5 100644 --- a/app/models/beach_api_core/plan.rb +++ b/app/models/beach_api_core/plan.rb @@ -1,10 +1,77 @@ module BeachApiCore class Plan < ApplicationRecord - validates :name, presence: true + require 'stripe' + validates :name, :plan_for, :interval, :stripe_id, :currency, :amount, presence: true + validates :users_count, numericality: {less_than_or_equal_to: 10}, if: :organisation? + validates :amount, numericality: {greater_than: 0} + validates :users_count, :amount_per_additional_user, numericality: {greater_than: 0}, presence: true, if: :organisation? + validates :stripe_id, :name, uniqueness: true + validate :create_stripe_plan, on: [:create] has_many :organisation_plans, dependent: :destroy has_many :plan_items, dependent: :destroy + has_many :users, class_name: "BeachApiCore::User" + has_many :subscriptions, class_name: "BeachApiCore::Subscription", dependent: :destroy + + before_destroy :delete_stripe_plan + accepts_nested_attributes_for :plan_items, allow_destroy: true + attr_readonly :plan_for, :interval, :stripe_id, :amount, :amount_per_additional_user, :users_count, :currency + + Stripe.api_key = ENV['STRIPE_SECRET_KEY'] + + def organisation? + self.plan_for == "organisation" + end + + def delete_stripe_plan + begin + stripe_plan = Stripe::Plan.retrieve(self.stripe_id) + stripe_product = Stripe::Product.retrieve(stripe_plan.product) + stripe_plan.delete + stripe_product.delete + rescue => e + self.errors.add e.message unless e.message.match?(/No such plan/) + end + throw(:abort) if self.errors.present? + end + + def create_stripe_plan + return unless self.errors.blank? + trial = self.trial_period_days.nil? ? 0 : self.trial_period_days + + if self.organisation? + Stripe::Plan.create( + { + interval: self.interval, + trial_period_days: trial, + product: { + name: self.name, + }, + billing_scheme: "tiered", + tiers: [{flat_amount: self.amount, up_to: self.users_count},{unit_amount: self.amount_per_additional_user, up_to:"inf"}], + tiers_mode: "graduated", + currency: self.currency, + id: self.stripe_id, + } + ) + else + self.users_count = 1 + self.amount_per_additional_user = 0 + Stripe::Plan.create( + { + amount: self.amount, + interval: self.interval, + trial_period_days: trial, + product: { name: self.name }, + currency: self.currency, + id: self.stripe_id, + } + ) + end + rescue => e + self.errors.add :stripe_error, e.message + end end end diff --git a/app/models/beach_api_core/subscription.rb b/app/models/beach_api_core/subscription.rb new file mode 100644 index 0000000..f1260aa --- /dev/null +++ b/app/models/beach_api_core/subscription.rb @@ -0,0 +1,99 @@ +module BeachApiCore + class Subscription < ApplicationRecord + belongs_to :plan, class_name: "BeachApiCore::Plan" + belongs_to :owner, polymorphic: true + + validates :owner_type, :owner_id, :plan_id, presence: true + validate :check_subscription_for, on: [:create, :update] + validate :create_subscription, on: [:create] + validate :change_subscription, on: [:update] + + before_destroy :cancel_subscription + attr_accessor :not_change, :user_id, :organisation_id + + Stripe.api_key = ENV['STRIPE_SECRET_KEY'] + + def create_subscription + self.errors.add :subscription, "can't be created because you have active subscription" unless self.owner.subscription.nil? + return unless errors.blank? + if self.owner.nil? || self.owner.stripe_customer_token.nil? + self.errors.add :owner, 'wrong subscription owner' + else + begin + subs = Stripe::Subscription.create( + { + customer: self.owner.stripe_customer_token, + items: [ + { + plan: self.plan.stripe_id, + quantity: 1 + } + ] + } + ) + Stripe::Subscription.update(subs.id, {quantity: self.get_quantity}) if self.owner_type == "BeachApiCore::Organisation" + self.stripe_subscription_id = subs.id + if self.owner_type == 'BeachApiCore::Organisation' && !not_change + self.owner.organisation_plan.nil? ? BeachApiCore::OrganisationPlan.create(:plan_id => self.plan_id, organisation_id: self.owner_id) : self.owner.organisation_plan.update_attribute(:plan_id, self.plan_id) + end + rescue => e + self.errors.add :stripe_error, e.message + end + end + end + + def change_subscription + return unless self.plan_id_changed? + return unless errors.blank? + begin + subscription = Stripe::Subscription.retrieve(self.stripe_subscription_id) + Stripe::Subscription.update( + self.stripe_subscription_id, + { + cancel_at_period_end: false, + items: [ + { + id: subscription.items.data[0].id, + plan: self.plan.stripe_id, + } + ] + } + ) + Stripe::Subscription.update(subs.id, {quantity: self.get_quantity}) if self.owner_type == "BeachApiCore::Organisation" + if owner_type == 'BeachApiCore::Organisation' && !not_change + self.owner.organisation_plan.nil? ? BeachApiCore::OrganisationPlan.create(:plan_id => self.plan_id, organisation_id: self.owner_id) : self.owner.organisation_plan.update_attribute(:plan_id, self.plan_id) + end + rescue => e + self.errors.add :stripe_error, e.message + end + end + + def cancel_subscription + begin + sub = Stripe::Subscription.retrieve(self.stripe_subscription_id) + if sub.current_period_end > Time.now.to_i + invoice = Stripe::Invoice.retrieve(sub.latest_invoice) + time_left = sub.current_period_end - Time.now.to_i + refund_sum = (time_left * invoice.amount_due)/(sub.current_period_end-sub.current_period_start) + Stripe::Refund.create({charge: invoice.charge, amount: refund_sum}) + end + sub.delete + rescue => e + self.errors.add :error, e.message unless e.message.match?(/No such subscription:/) + end + throw(:abort) if self.errors.present? + BeachApiCore::OrganisationPlan.find_by(:organisation_id => self.owner_id, :plan_id => self.plan_id).destroy if self.owner_type == "BeachApiCore::Organisation" && !self.owner.plan.nil? + end + + def get_quantity + quantity = BeachApiCore::Membership.where(:group_type => self.owner_type, :group_id => self.owner_id).pluck(:member_id, :group_type, :group_id).uniq.count + quantity + end + + private + def check_subscription_for + self.errors.add :plan, "wrong for indicated type" unless self.owner_type.gsub("BeachApiCore::",'').downcase == self.plan.plan_for + end + end + +end \ No newline at end of file diff --git a/app/models/beach_api_core/user.rb b/app/models/beach_api_core/user.rb index 57a68d9..9b9a493 100644 --- a/app/models/beach_api_core/user.rb +++ b/app/models/beach_api_core/user.rb @@ -3,7 +3,6 @@ class User < ApplicationRecord include Concerns::UserConfirm include Concerns::UserRoles include Concerns::UserPermissions - include Redis::Objects attr_accessor :require_confirmation, :require_current_password, :current_password, :confirmed, :application_id, :from_admin @@ -35,6 +34,7 @@ class User < ApplicationRecord has_many :projects, class_name: 'BeachApiCore::Project', inverse_of: :user, dependent: :destroy has_many :entities, inverse_of: :user, dependent: :destroy + has_many :invoices, class_name: 'BeachApiCore::Invoice', as: :keeper, dependent: :destroy has_many :interactions, inverse_of: :user, dependent: :destroy has_many :chats_users, class_name: 'BeachApiCore::Chat::ChatsUser', inverse_of: :user has_many :chats, through: :chats_users @@ -47,6 +47,8 @@ class User < ApplicationRecord has_many :organisation_accesses, -> { where(keeper_type: 'BeachApiCore::Organisation') }, inverse_of: :user, class_name: 'BeachApiCore::UserAccess' has_many :invites, class_name: "BeachApiCore::Invite", dependent: :destroy + has_one :subscription, class_name: "BeachApiCore::Subscription", as: :owner, dependent: :destroy + has_many :organisations_subscriptions, class_name: "BeachApiCore::Organisation", foreign_key: :subscription_owner_id validates :email, presence: true, @@ -78,10 +80,10 @@ class User < ApplicationRecord after_initialize :set_defaults after_create :set_sex_and_birth_date after_create :send_email_confirmation, if: :from_admin + before_destroy :destroy_stripe_customer delegate :first_name, :last_name, :name, to: :profile - set :sessions # Redis set enum status: %i(active invitee) SCORES_MESSAGE = "Your scores was changed." @@ -94,6 +96,13 @@ def search(term) end end + def destroy_stripe_customer + Stripe.api_key = ENV['STRIPE_SECRET_KEY'] + + customer = Stripe::Customer.retrieve(self.stripe_customer_token) + customer.delete + end + def confirmed? confirmed_at.present? end @@ -172,7 +181,6 @@ def generate_username uniq_number = BeachApiCore::User.maximum(:id).to_i + 1 self.username = "#{Regexp.last_match[1]}-#{uniq_number}" if email =~ /\A(.*)@/ end - end end diff --git a/app/serializers/beach_api_core/plan_serializer.rb b/app/serializers/beach_api_core/plan_serializer.rb new file mode 100644 index 0000000..b8ee381 --- /dev/null +++ b/app/serializers/beach_api_core/plan_serializer.rb @@ -0,0 +1,8 @@ +module BeachApiCore + class PlanSerializer < ActiveModel::Serializer + include BeachApiCore::Concerns::DocIdAbsSerializerConcern + acts_as_abs_doc_id + + attributes :id, :stripe_id, :name, :amount, :interval, :plan_for + end +end \ No newline at end of file diff --git a/app/serializers/beach_api_core/subscription_serializer.rb b/app/serializers/beach_api_core/subscription_serializer.rb new file mode 100644 index 0000000..5ef5afd --- /dev/null +++ b/app/serializers/beach_api_core/subscription_serializer.rb @@ -0,0 +1,13 @@ +module BeachApiCore + class SubscriptionSerializer < ActiveModel::Serializer + include BeachApiCore::Concerns::DocIdAbsSerializerConcern + acts_as_abs_doc_id + + attributes :id, :subscription_for + has_one :plan + + def subscription_for + object.owner_type == 'BeachApiCore::Organisation' ? 'organisation' : 'user' + end + end +end \ No newline at end of file diff --git a/beach_api_core.gemspec b/beach_api_core.gemspec index 8a4fd7c..9b385e3 100644 --- a/beach_api_core.gemspec +++ b/beach_api_core.gemspec @@ -42,6 +42,7 @@ Gem::Specification.new do |s| s.add_dependency 'interactor-rails' s.add_dependency 'slim-rails' + s.add_dependency 'stripe' s.add_dependency 'apipie-rails' s.add_dependency 'rmagick' @@ -50,7 +51,6 @@ Gem::Specification.new do |s| s.add_dependency 'aws-sdk' s.add_dependency 'activeadmin' s.add_dependency 'paper_trail' - s.add_dependency 'redis-objects' s.add_dependency 'ancestry' s.add_dependency 'rack-cors' s.add_dependency 'rails-jquery-autocomplete' diff --git a/config/locales/en.yml b/config/locales/en.yml index df65f99..4c898cf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,6 +16,8 @@ en: authorization: "Authorization" passwords: "Passwords" broadcasts: 'Broadcasts' + subscriptions: 'Subscriptions' + plans: 'Plans' errors: forbidden_request: "Forbidden request" unauthorized: "Unauthorized" diff --git a/config/routes.rb b/config/routes.rb index d826036..2af6cde 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,7 +42,12 @@ resource :password, only: %i(create update), defaults: { format: false } get "/password/restore_password/:token", to: "passwords#restore_password", defaults: { format: false } get "/password/success", to: "passwords#success_restore", defaults: { format: false } - resource :user, only: %i(show update) + resources :users, only: %i(show update) do + resources :subscriptions, only: %i(show), controller: "subscriptions" do + get :show, on: :collection + get :show_invoices, on: :member + end + end resources :services, only: %i(index update) do resource :capabilities, only: %i(create destroy) end @@ -58,6 +63,9 @@ get :users get :get_current end + resources :subscriptions, only: %i(show), controller: "subscriptions" do + get :show_invoices, on: :member + end put :current, on: :member get :published_applications, on: :member end @@ -92,5 +100,11 @@ put :read, on: :member resources :messages, only: %i(create index) end + resources :plans, only: [:index, :create, :show, :destroy] + resources :subscriptions, only: [:create, :update, :show, :destroy] do + post :create_customer, on: :collection, only: %i(create) + post :update_quantity, on: :member + post :invoice_created, on: :collection + end end end diff --git a/db/migrate/20190305065454_add_stripe_columns_to_plan.rb b/db/migrate/20190305065454_add_stripe_columns_to_plan.rb new file mode 100644 index 0000000..308a565 --- /dev/null +++ b/db/migrate/20190305065454_add_stripe_columns_to_plan.rb @@ -0,0 +1,10 @@ +class AddStripeColumnsToPlan < ActiveRecord::Migration[5.1] + def change + add_column :beach_api_core_plans, :stripe_id, :string + add_column :beach_api_core_plans, :amount, :integer + add_column :beach_api_core_plans, :interval, :string + add_column :beach_api_core_plans, :interval_amount, :integer + add_column :beach_api_core_plans, :trial_period_days, :integer + add_column :beach_api_core_plans, :plan_for, :integer + end +end diff --git a/db/migrate/20190305092002_add_stripe_card_token_to_user.rb b/db/migrate/20190305092002_add_stripe_card_token_to_user.rb new file mode 100644 index 0000000..a3221f1 --- /dev/null +++ b/db/migrate/20190305092002_add_stripe_card_token_to_user.rb @@ -0,0 +1,5 @@ +class AddStripeCardTokenToUser < ActiveRecord::Migration[5.1] + def change + add_column :beach_api_core_users, :stripe_customer_token, :string + end +end diff --git a/db/migrate/20190306075513_add_subscription_table.rb b/db/migrate/20190306075513_add_subscription_table.rb new file mode 100644 index 0000000..f774225 --- /dev/null +++ b/db/migrate/20190306075513_add_subscription_table.rb @@ -0,0 +1,9 @@ +class AddSubscriptionTable < ActiveRecord::Migration[5.1] + def change + create_table :beach_api_core_subscriptions do |t| + t.references :plan + t.references :owner, polymorphic: true, index: true + t.string :stripe_subscription_id + end + end +end diff --git a/db/migrate/20190306084950_add_subscription_owner_to_organisation.rb b/db/migrate/20190306084950_add_subscription_owner_to_organisation.rb new file mode 100644 index 0000000..9d82951 --- /dev/null +++ b/db/migrate/20190306084950_add_subscription_owner_to_organisation.rb @@ -0,0 +1,5 @@ +class AddSubscriptionOwnerToOrganisation < ActiveRecord::Migration[5.1] + def change + add_reference :beach_api_core_organisations, :subscription_owner + end +end diff --git a/db/migrate/20190307090616_add_users_count_and_additional_amount_to_plan.rb b/db/migrate/20190307090616_add_users_count_and_additional_amount_to_plan.rb new file mode 100644 index 0000000..94666a2 --- /dev/null +++ b/db/migrate/20190307090616_add_users_count_and_additional_amount_to_plan.rb @@ -0,0 +1,6 @@ +class AddUsersCountAndAdditionalAmountToPlan < ActiveRecord::Migration[5.1] + def change + add_column :beach_api_core_plans, :users_count, :integer + add_column :beach_api_core_plans, :amount_per_additional_user, :integer + end +end diff --git a/db/migrate/20191128114326_add_stripe_customer_token_column_to_organisation.rb b/db/migrate/20191128114326_add_stripe_customer_token_column_to_organisation.rb new file mode 100644 index 0000000..9287696 --- /dev/null +++ b/db/migrate/20191128114326_add_stripe_customer_token_column_to_organisation.rb @@ -0,0 +1,5 @@ +class AddStripeCustomerTokenColumnToOrganisation < ActiveRecord::Migration[5.1] + def change + add_column :beach_api_core_organisations, :stripe_customer_token, :string + end +end diff --git a/db/migrate/20191209113315_add_currency_to_plans.rb b/db/migrate/20191209113315_add_currency_to_plans.rb new file mode 100644 index 0000000..d68cc27 --- /dev/null +++ b/db/migrate/20191209113315_add_currency_to_plans.rb @@ -0,0 +1,5 @@ +class AddCurrencyToPlans < ActiveRecord::Migration[5.1] + def change + add_column :beach_api_core_plans, :currency, :string + end +end diff --git a/db/migrate/20191210130832_create_beach_api_core_invoices.rb b/db/migrate/20191210130832_create_beach_api_core_invoices.rb new file mode 100644 index 0000000..87a4905 --- /dev/null +++ b/db/migrate/20191210130832_create_beach_api_core_invoices.rb @@ -0,0 +1,13 @@ +class CreateBeachApiCoreInvoices < ActiveRecord::Migration[5.1] + def change + create_table :beach_api_core_invoices do |t| + t.string :keeper_type + t.integer :keeper_id + t.integer :subscription_id + t.string :invoice_url_link + t.string :invoice_pdf_link + t.boolean :payment_successfull + t.timestamps + end + end +end diff --git a/db/migrate/20191220130725_change_plan_for_column_in_plan.rb b/db/migrate/20191220130725_change_plan_for_column_in_plan.rb new file mode 100644 index 0000000..9d5cbf0 --- /dev/null +++ b/db/migrate/20191220130725_change_plan_for_column_in_plan.rb @@ -0,0 +1,5 @@ +class ChangePlanForColumnInPlan < ActiveRecord::Migration[5.1] + def change + change_column(:beach_api_core_plans, :plan_for, :string) + end +end diff --git a/lib/admin/invoices.rb b/lib/admin/invoices.rb new file mode 100644 index 0000000..28085ad --- /dev/null +++ b/lib/admin/invoices.rb @@ -0,0 +1,40 @@ +ActiveAdmin.register BeachApiCore::Invoice, as: 'Invoice' do + menu parent: 'Services' + permit_params :keeper_type, :keeper_id, :subscription_id, :invoice_url_link, :invoice_pdf_link, :payment_successfull + + index do + id_column + column :keeper_type + column :keeper_id + column :subscription_id + column :invoice_url_link + column :invoice_pdf_link + column :payment_successfull + actions + end + + filter :name + + form do |f| + f.inputs label:"Invoice" do + f.input :keeper_type, as: :select, collection: [["Organisation","BeachApiCore::Organisation"],["User","BeachApiCore::User"]] + f.input :keeper_id, label: "ID keeper" + f.input :subscription_id, label: "ID subscription" + f.input :invoice_url_link, label: "Url" + f.input :invoice_pdf_link, label: "Pdf" + f.input :payment_successfull + end + f.actions + end + + show do |_plan| + attributes_table do + row :keeper_type + row :keeper_id + row :subscription_id + row :invoice_url_link + row :invoice_pdf_link + row :payment_successfull + end + end +end \ No newline at end of file diff --git a/lib/admin/organisations.rb b/lib/admin/organisations.rb index b5ed8d5..303c4bd 100644 --- a/lib/admin/organisations.rb +++ b/lib/admin/organisations.rb @@ -1,7 +1,7 @@ ActiveAdmin.register BeachApiCore::Organisation, as: 'Organisation' do menu parent: 'Organisations' - permit_params :name, :application_id, :send_email, + permit_params :name, :application_id, :send_email, :email, logo_image_attributes: %i(id file), organisation_plan_attributes: %i(plan_id) @@ -19,6 +19,7 @@ f.object.build_logo_image if f.object.logo_image.blank? f.object.build_organisation_plan if f.object.organisation_plan.blank? f.inputs t('active_admin.details', model: t('activerecord.models.organisation.one')) do + f.semantic_errors *f.object.errors.keys f.input :name f.input :application, as: :select, collection: Doorkeeper::Application.all li do @@ -30,6 +31,7 @@ div { image_tag attachment_url(f.object.logo_image, :file, :fill, 150, 150) } end end + f.input :email f.fields_for :organisation_plan do |p| p.input :plan, as: :select, collection: BeachApiCore::Plan.all end @@ -42,6 +44,7 @@ attributes_table do row :name row :application + row :email if organisation.logo_image.present? row :logo do image_tag attachment_url(organisation.logo_image, :file, :fill, 150, 150) diff --git a/lib/admin/plans.rb b/lib/admin/plans.rb index d69fa7b..fc095b0 100644 --- a/lib/admin/plans.rb +++ b/lib/admin/plans.rb @@ -1,11 +1,13 @@ ActiveAdmin.register BeachApiCore::Plan, as: 'Plan' do menu parent: 'Organisations' - permit_params :name, plan_items_attributes: %i(id access_level_id users_count) + permit_params :name, :amount, :stripe_id, :interval, :plan_for, :amount_per_additional_user, :billing_scheme, :users_count, :trial_period_days, :tiers, :tiers_mode, :currency, + plan_items_attributes: %i(id access_level_id users_count) index do id_column column :name + column :stripe_id actions end @@ -14,11 +16,30 @@ form do |f| f.inputs t('active_admin.details', model: t('activerecord.models.plan.one')) do f.input :name + f.input :amount + if f.object.new_record? + f.input :stripe_id + f.input :interval, as: :select, collection: [["Day", "day"], ["Month", "month"], ["Year", "year"]] + f.input :plan_for, as: :select, collection: [["Organisation","organisation"], ["User","user"]] + f.input :users_count + f.input :amount_per_additional_user + f.input :currency, as: :select, collection: Stripe::CountrySpec.list.pluck("supported_payment_currencies").flatten.uniq + f.input :trial_period_days + else + f.input :stripe_id, input_html: {disabled: true} + f.input :interval, as: :select, collection: [["Day", "day"], ["Month", "month"], ["Year", "year"]], input_html: {disabled: true} + f.input :plan_for, as: :select, collection: [["Organisation", "organisation"], ["User", "user"]], input_html: {disabled: true} + f.input :users_count, input_html: {disabled: true} + f.input :amount_per_additional_user, input_html: {disabled: true} + f.input :currency, as: :select, collection: Stripe::CountrySpec.list.pluck("supported_payment_currencies").flatten.uniq, input_html: {disabled: true} + f.input :trial_period_days, input_html: {disabled: true} + end f.has_many :plan_items, allow_destroy: true, heading: t('activerecord.models.plan_option.other') do |o| o.input :access_level, as: :select, collection: BeachApiCore::AccessLevel.all, label: t('activerecord.models.access_level.one') o.input :users_count + o.input :users_count end end f.actions @@ -27,6 +48,10 @@ show do |_plan| attributes_table do row :name + row :stripe_id + row :amount + row :interval + row :plan_for if _plan.plan_items.any? panel 'Plan_Options' do table_for _plan.plan_items do diff --git a/lib/admin/rewards.rb b/lib/admin/rewards.rb index 2d321d1..d0350fd 100644 --- a/lib/admin/rewards.rb +++ b/lib/admin/rewards.rb @@ -36,9 +36,9 @@ reward.gift_uuid.present? && reward.Fulfilled? end - action_item only: :index, priority: 0 do - link_to('Run Worker', admin_run_reward_worker_path) - end + # action_item only: :index, priority: 0 do + # link_to('Run Worker', admin_run_reward_worker_path) + # end index do diff --git a/lib/admin/subscriptions.rb b/lib/admin/subscriptions.rb new file mode 100644 index 0000000..0940549 --- /dev/null +++ b/lib/admin/subscriptions.rb @@ -0,0 +1,37 @@ +ActiveAdmin.register BeachApiCore::Subscription, as: 'Subscription' do + menu priority: 66, parent: 'Services' + + permit_params :plan_id, :owner_type, :owner_id, :user_id, :organisation_id + + form do |f| + + f.inputs do + #f.semantic_errors *f.object.errors.keys + + f.input :plan + if f.object.new_record? + f.input :owner_type, + as: :select, + collection: [ + ['User', 'BeachApiCore::User'], + ['Organisation', 'BeachApiCore::Organisation'] + ] + f.input :user_id, input_html: { id: "owner_user_input" }, as: :select, collection: BeachApiCore::User.all + f.input :organisation_id, input_html: { id: "owner_organisation_input" }, as: :select, collection: BeachApiCore::Organisation.all + end + f.actions + end + end + + controller do + def create + case params[:subscription][:owner_type] + when 'BeachApiCore::User' + params[:subscription][:owner_id] = params[:subscription][:user_id] + when 'BeachApiCore::Organisation' + params[:subscription][:owner_id] = params[:subscription][:organisation_id] + end + super + end + end +end diff --git a/lib/beach_api_core/engine.rb b/lib/beach_api_core/engine.rb index 2fa07c6..75e9aca 100644 --- a/lib/beach_api_core/engine.rb +++ b/lib/beach_api_core/engine.rb @@ -14,7 +14,6 @@ require 'rails-jquery-autocomplete' require 'engine_store' require 'paper_trail' -require 'redis/objects' require 'rack/cors' module BeachApiCore diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 65d6fb0..f7fcc98 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190110100006) do +ActiveRecord::Schema.define(version: 20181205063212) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -24,32 +24,6 @@ t.index ["name"], name: "index_beach_api_core_access_levels_on_name" end - create_table "beach_api_core_achievement_brands", force: :cascade do |t| - t.bigint "achievement_id" - t.bigint "giftbit_brand_id" - t.index ["achievement_id"], name: "index_beach_api_core_achievement_brands_on_achievement_id" - t.index ["giftbit_brand_id"], name: "index_beach_api_core_achievement_brands_on_giftbit_brand_id" - end - - create_table "beach_api_core_achievements", force: :cascade do |t| - t.bigint "application_id" - t.string "achievement_name" - t.integer "points_required" - t.integer "max_rewards" - t.integer "reward_expiry", default: 0 - t.boolean "reward_issue_requires_approval", default: false - t.boolean "notify_by_email", default: false - t.string "mode_type" - t.bigint "mode_id" - t.boolean "notify_via_broadcasts", default: false - t.integer "available_for", default: 0 - t.bigint "giftbit_config_id" - t.boolean "use_all_config_brands", default: false - t.index ["application_id"], name: "index_beach_api_core_achievements_on_application_id" - t.index ["giftbit_config_id"], name: "index_beach_api_core_achievements_on_giftbit_config_id" - t.index ["mode_type", "mode_id"], name: "index_beach_api_core_achievements_on_mode_type_and_mode_id" - end - create_table "beach_api_core_assets", id: :serial, force: :cascade do |t| t.string "file_id", null: false t.string "file_filename" @@ -166,14 +140,6 @@ t.index ["application_id"], name: "index_beach_api_core_custom_views_on_application_id" end - create_table "beach_api_core_device_scores", force: :cascade do |t| - t.bigint "device_id" - t.bigint "application_id" - t.integer "scores", default: 0 - t.index ["application_id"], name: "index_beach_api_core_device_scores_on_application_id" - t.index ["device_id"], name: "index_beach_api_core_device_scores_on_device_id" - end - create_table "beach_api_core_devices", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false @@ -225,25 +191,6 @@ t.index ["user_id"], name: "index_beach_api_core_favourites_on_user_id" end - create_table "beach_api_core_giftbit_brands", force: :cascade do |t| - t.bigint "giftbit_config_id" - t.string "gift_name" - t.integer "amount" - t.string "brand_code" - t.string "giftbit_email_template" - t.string "email_subject" - t.text "email_body" - t.index ["giftbit_config_id"], name: "index_beach_api_core_giftbit_brands_on_giftbit_config_id" - end - - create_table "beach_api_core_giftbit_configs", force: :cascade do |t| - t.bigint "application_id" - t.string "giftbit_token" - t.string "config_name" - t.string "email_to_notify" - t.index ["application_id"], name: "index_beach_api_core_giftbit_configs_on_application_id" - end - create_table "beach_api_core_instances", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false @@ -325,6 +272,16 @@ t.index ["user_id"], name: "index_beach_api_core_invites_on_user_id" end + create_table "beach_api_core_invoices", force: :cascade do |t| + t.string "keeper_type" + t.integer "keeper_id" + t.integer "subscription_id" + t.string "invoice_url_link" + t.string "invoice_pdf_link" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "beach_api_core_jobs", force: :cascade do |t| t.datetime "start_at" t.text "params" @@ -380,6 +337,7 @@ t.datetime "updated_at", null: false t.hstore "logo_properties" t.boolean "send_email", default: false + t.string "stripe_customer_token" t.index ["application_id"], name: "index_beach_api_core_organisations_on_application_id" end @@ -411,6 +369,18 @@ t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "stripe_id" + t.integer "amount" + t.string "interval" + t.integer "interval_amount" + t.integer "trial_period_days" + t.integer "plan_for" + t.integer "users_count" + t.integer "amount_per_additional_user" + t.json "tiers", default: [], array: true + t.string "billing_scheme" + t.string "currency" + t.string "tiers_mode" end create_table "beach_api_core_profile_attributes", id: :serial, force: :cascade do |t| @@ -468,23 +438,6 @@ t.index ["user_id"], name: "index_beach_api_core_projects_on_user_id" end - create_table "beach_api_core_rewards", force: :cascade do |t| - t.bigint "achievement_id" - t.string "reward_to_type" - t.bigint "reward_to_id" - t.boolean "confirmed", default: false - t.string "gift_uuid" - t.string "campaign_uuid" - t.string "shortlink" - t.integer "status", default: 0 - t.bigint "giftbit_brand_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["achievement_id"], name: "index_beach_api_core_rewards_on_achievement_id" - t.index ["giftbit_brand_id"], name: "index_beach_api_core_rewards_on_giftbit_brand_id" - t.index ["reward_to_type", "reward_to_id"], name: "index_beach_api_core_rewards_on_reward_to_type_and_reward_to_id" - end - create_table "beach_api_core_roles", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false @@ -577,22 +530,6 @@ t.index ["username"], name: "index_beach_api_core_users_on_username" end - create_table "beach_api_core_webhook_configs", force: :cascade do |t| - t.bigint "application_id" - t.string "uri" - t.integer "request_method", default: 0 - t.text "request_body" - t.string "config_name" - t.index ["application_id"], name: "index_beach_api_core_webhook_configs_on_application_id" - end - - create_table "beach_api_core_webhook_parametrs", force: :cascade do |t| - t.bigint "webhook_config_id" - t.string "name" - t.string "value" - t.index ["webhook_config_id"], name: "index_beach_api_core_webhook_parametrs_on_webhook_config_id" - end - create_table "beach_api_core_webhooks", force: :cascade do |t| t.string "uri", null: false t.integer "kind", null: false diff --git a/spec/models/beach_api_core/invoice_spec.rb b/spec/models/beach_api_core/invoice_spec.rb new file mode 100644 index 0000000..c460664 --- /dev/null +++ b/spec/models/beach_api_core/invoice_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module BeachApiCore + RSpec.describe Invoice, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/requests/beach_api_core/v1/invitation_request_spec.rb b/spec/requests/beach_api_core/v1/invitation_request_spec.rb index 25f36a3..b7827b9 100644 --- a/spec/requests/beach_api_core/v1/invitation_request_spec.rb +++ b/spec/requests/beach_api_core/v1/invitation_request_spec.rb @@ -88,7 +88,7 @@ module BeachApiCore }, headers: bearer_auth end.to change(Invitation, :count).by(1) - .and change(ActionMailer::Base.deliveries, :count).by(1) + .and change(ActionMailer::Base.deliveries, :count).by(0) expect(response.status).to eq 201 end end