diff --git a/app/controllers/admin/connected_apps_controller.rb b/app/controllers/admin/connected_apps_controller.rb index 2cbaca7c9b..3531c7371a 100644 --- a/app/controllers/admin/connected_apps_controller.rb +++ b/app/controllers/admin/connected_apps_controller.rb @@ -5,12 +5,12 @@ module Admin def create authorize! :admin, enterprise - app = ConnectedApp.create!(enterprise_id: enterprise.id) + attributes = {} + attributes[:type] = connected_app_params[:type] if connected_app_params[:type] - ConnectAppJob.perform_later( - app, spree_current_user.spree_api_key, - channel: SessionChannel.for_request(request), - ) + app = ConnectedApp.create!(enterprise_id: enterprise.id, **attributes) + app.connect(api_key: spree_current_user.spree_api_key, + channel: SessionChannel.for_request(request)) render_panel end @@ -18,15 +18,9 @@ module Admin def destroy authorize! :admin, enterprise - app = enterprise.connected_apps.first + app = enterprise.connected_apps.find(params.require(:id)) app.destroy - WebhookDeliveryJob.perform_later( - app.data["destroy"], - "disconnect-app", - nil - ) - render_panel end @@ -39,5 +33,9 @@ module Admin def render_panel redirect_to "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel" end + + def connected_app_params + params.permit(:type) + end end end diff --git a/app/jobs/connect_app_job.rb b/app/jobs/connect_app_job.rb index 2474343a29..fa1b802d05 100644 --- a/app/jobs/connect_app_job.rb +++ b/app/jobs/connect_app_job.rb @@ -19,8 +19,8 @@ class ConnectAppJob < ApplicationJob selector = "#connected-app-discover-regen.enterprise_#{enterprise.id}" html = ApplicationController.render( - partial: "admin/enterprises/form/connected_apps", - locals: { enterprise: }, + partial: "admin/enterprises/form/connected_apps/discover_regen", + locals: { enterprise:, connected_app: enterprise.connected_apps.discover_regen.first }, ) cable_ready[channel].morph(selector:, html:).broadcast diff --git a/app/models/connected_app.rb b/app/models/connected_app.rb index 3edc7c4147..9c99ba7dcc 100644 --- a/app/models/connected_app.rb +++ b/app/models/connected_app.rb @@ -5,7 +5,31 @@ # Here we store keys and links to access the app. class ConnectedApp < ApplicationRecord belongs_to :enterprise + after_destroy :disconnect + + scope :discover_regen, -> { where(type: "ConnectedApp") } + scope :affiliate_sales_data, -> { where(type: "ConnectedApps::AffiliateSalesData") } scope :connecting, -> { where(data: nil) } scope :ready, -> { where.not(data: nil) } + + def connecting? + data.nil? + end + + def ready? + !connecting? + end + + def connect(api_key:, channel:) + ConnectAppJob.perform_later(self, api_key, channel:) + end + + def disconnect + WebhookDeliveryJob.perform_later( + data["destroy"], + "disconnect-app", + nil + ) + end end diff --git a/app/models/connected_apps/affiliate_sales_data.rb b/app/models/connected_apps/affiliate_sales_data.rb new file mode 100644 index 0000000000..51d9d1b5df --- /dev/null +++ b/app/models/connected_apps/affiliate_sales_data.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# An enterprise can opt-in for their data to be included in the affiliate_sales_data endpoint +# +module ConnectedApps + class AffiliateSalesData < ConnectedApp + def connect(_opts) + update! data: true # not-nil value indicates it is ready + end + + def disconnect; end + end +end diff --git a/app/models/spree/user.rb b/app/models/spree/user.rb index f7ceb5ddca..bef7609933 100644 --- a/app/models/spree/user.rb +++ b/app/models/spree/user.rb @@ -156,6 +156,12 @@ module Spree self.disabled_at = value == '1' ? Time.zone.now : nil end + def affiliate_enterprises + return [] unless Flipper.enabled?(:affiliate_sales_data, self) + + Enterprise.joins(:connected_apps).merge(ConnectedApps::AffiliateSalesData.ready) + end + protected def password_required? diff --git a/app/views/admin/enterprises/form/_connected_apps.html.haml b/app/views/admin/enterprises/form/_connected_apps.html.haml index 2ffbceee81..1c7904b2ed 100644 --- a/app/views/admin/enterprises/form/_connected_apps.html.haml +++ b/app/views/admin/enterprises/form/_connected_apps.html.haml @@ -1,30 +1,4 @@ -%div{ id: "connected-app-discover-regen", class: "enterprise_#{enterprise.id}" } - .connected-app__head - %div - %h3= t ".title" - %p= t ".tagline" - %div - - if enterprise.connected_apps.empty? - = button_to t(".enable"), admin_enterprise_connected_apps_path(enterprise.id), method: :post, disabled: !managed_by_user?(enterprise) - -# This is only seen by super-admins: - %em= t(".need_to_be_manager") unless managed_by_user?(enterprise) - - elsif enterprise.connected_apps.connecting.present? - %button{ disabled: true } - %i.spinner.fa.fa-spin.fa-circle-o-notch -   - = t ".loading" - - else - = button_to t(".disable"), admin_enterprise_connected_app_path(0, enterprise_id: enterprise.id), method: :delete - - .connected-app__connection - - if enterprise.connected_apps.ready.present? - .connected-app__note - - link = enterprise.connected_apps[0].data["link"] - %p= t ".note" - %div - %a{ href: link, target: "_blank", class: "button secondary" } - = t ".link_label" - - %hr - .connected-app__description - = t ".description_html" += render partial: "/admin/enterprises/form/connected_apps/discover_regen", + locals: { enterprise:, connected_app: enterprise.connected_apps.discover_regen.first } += render partial: "/admin/enterprises/form/connected_apps/affiliate_sales_data", + locals: { enterprise:, connected_app: enterprise.connected_apps.affiliate_sales_data.first } diff --git a/app/views/admin/enterprises/form/connected_apps/_affiliate_sales_data.html.haml b/app/views/admin/enterprises/form/connected_apps/_affiliate_sales_data.html.haml new file mode 100644 index 0000000000..52ec355a35 --- /dev/null +++ b/app/views/admin/enterprises/form/connected_apps/_affiliate_sales_data.html.haml @@ -0,0 +1,15 @@ +%section.connected_app + .connected-app__head + %div + %h3= t ".title" + %p= t ".tagline" + %div + - if connected_app.nil? + = button_to t(".enable"), admin_enterprise_connected_apps_path(enterprise.id, type: "ConnectedApps::AffiliateSalesData"), method: :post, disabled: !managed_by_user?(enterprise) + -# This is only seen by super-admins: + %em= t(".need_to_be_manager") unless managed_by_user?(enterprise) + - else + = button_to t(".disable"), admin_enterprise_connected_app_path(connected_app.id, enterprise_id: enterprise.id), method: :delete + %hr + .connected-app__description + = t ".description_html" diff --git a/app/views/admin/enterprises/form/connected_apps/_discover_regen.html.haml b/app/views/admin/enterprises/form/connected_apps/_discover_regen.html.haml new file mode 100644 index 0000000000..1bab8f75ec --- /dev/null +++ b/app/views/admin/enterprises/form/connected_apps/_discover_regen.html.haml @@ -0,0 +1,30 @@ +%section.connected_app{ id: "connected-app-discover-regen", class: "enterprise_#{enterprise.id}" } + .connected-app__head + %div + %h3= t ".title" + %p= t ".tagline" + %div + - if connected_app.nil? + = button_to t(".enable"), admin_enterprise_connected_apps_path(enterprise.id), method: :post, disabled: !managed_by_user?(enterprise) + -# This is only seen by super-admins: + %em= t(".need_to_be_manager") unless managed_by_user?(enterprise) + - elsif connected_app&.connecting? + %button{ disabled: true } + %i.spinner.fa.fa-spin.fa-circle-o-notch +   + = t ".loading" + - else + = button_to t(".disable"), admin_enterprise_connected_app_path(connected_app.id, enterprise_id: enterprise.id), method: :delete + + .connected-app__connection + - if connected_app&.ready? + .connected-app__note + - link = connected_app.data["link"] + %p= t ".note" + %div + %a{ href: link, target: "_blank", class: "button secondary" } + = t ".link_label" + + %hr + .connected-app__description + = t ".description_html" diff --git a/app/webpacker/css/admin/connected_apps.scss b/app/webpacker/css/admin/connected_apps.scss index c2cb2c9421..be3356009d 100644 --- a/app/webpacker/css/admin/connected_apps.scss +++ b/app/webpacker/css/admin/connected_apps.scss @@ -2,10 +2,18 @@ max-width: 615px; } +.connected_app { + margin-bottom: 2rem; + + &:not(:first-of-type) { + border-top: 1px solid $color-border; + } +} + .connected-app__head { display: flex; justify-content: space-between; - margin-bottom: 1em; + margin: 1em 0; h3 { margin-bottom: 1em; diff --git a/config/locales/en.yml b/config/locales/en.yml index d0764b7a91..7eb8c3dbf4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1358,28 +1358,45 @@ en: custom_tab_content: "Content for custom tab" connected_apps: legend: "Connected apps" - title: "Discover Regenerative" - tagline: "Allow Discover Regenerative to publish your enterprise information." - enable: "Allow data sharing" - disable: "Stop sharing" - loading: "Loading" - need_to_be_manager: "Only managers can connect apps." - note: | - Your Open Food Network account is connected to Discover Regenerative. - Add or update information on your Discover Regenerative listing here. - link_label: "Manage listing" - description_html: | -

- Eligible producers can showcase their regenerative credentials, - farming practices and more through a profile listing. - Simplifying how buyers can find regenerative produce and connect - with producers of interest. -

-

- Learn more about Discover Regenerative - -

+ affiliate_sales_data: + title: "INRAE / UFC QUE CHOISIR Research" + tagline: "Allow this research project to access your orders data anonymously" + enable: "Allow data sharing" + disable: "Stop sharing" + loading: "Loading" + need_to_be_manager: "Only managers can connect apps." + description_html: | +

+ INRAE and UFC QUE CHOISIR are teaming up to study food prices in short food systems and compare them with prices in the supermarket, for a given set of products. The data that is used by INRAE is mixed with data coming from other short food chain platforms in France. No individual product prices will be publicly disclosed through this project. +

+

+ Learn more about this research project + +

+ discover_regen: + title: "Discover Regenerative" + tagline: "Allow Discover Regenerative to publish your enterprise information." + enable: "Allow data sharing" + disable: "Stop sharing" + loading: "Loading" + need_to_be_manager: "Only managers can connect apps." + note: | + Your Open Food Network account is connected to Discover Regenerative. + Add or update information on your Discover Regenerative listing here. + link_label: "Manage listing" + description_html: | +

+ Eligible producers can showcase their regenerative credentials, + farming practices and more through a profile listing. + Simplifying how buyers can find regenerative produce and connect + with producers of interest. +

+

+ Learn more about Discover Regenerative + +

actions: edit_profile: Settings properties: Properties diff --git a/db/migrate/20240715072649_add_type_to_connected_apps.rb b/db/migrate/20240715072649_add_type_to_connected_apps.rb new file mode 100644 index 0000000000..5d75f999c8 --- /dev/null +++ b/db/migrate/20240715072649_add_type_to_connected_apps.rb @@ -0,0 +1,5 @@ +class AddTypeToConnectedApps < ActiveRecord::Migration[7.0] + def change + add_column :connected_apps, :type, :string, default: "ConnectedApp", null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index d18db6103f..2310ca953f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -68,6 +68,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_07_18_150852) do t.json "data" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "type", default: "ConnectedApp", null: false t.index ["enterprise_id"], name: "index_connected_apps_on_enterprise_id" end diff --git a/lib/tasks/enterprises.rake b/lib/tasks/enterprises.rake index fa735853c2..86c767dddd 100644 --- a/lib/tasks/enterprises.rake +++ b/lib/tasks/enterprises.rake @@ -12,6 +12,18 @@ namespace :ofn do enterprise.destroy end + namespace :enterprises do + desc "Activate connected app type for ALL enterprises" + task :activate_connected_app_type, [:type] => :environment do |_task, args| + Enterprise.find_each do |enterprise| + next if enterprise.connected_apps.public_send(args.type.underscore).exists? + + "ConnectedApps::#{args.type.camelize}".constantize.new(enterprise:).connect({}) + puts "Enterprise #{enterprise.id} connected." + end + end + end + namespace :dev do desc 'export enterprises to CSV' task export_enterprises: :environment do diff --git a/spec/fixtures/vcr_cassettes/Connected_Apps/can_be_enabled_and_disabled.yml b/spec/fixtures/vcr_cassettes/Connected_Apps/Discover_Regenerative/can_be_enabled_and_disabled.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/Connected_Apps/can_be_enabled_and_disabled.yml rename to spec/fixtures/vcr_cassettes/Connected_Apps/Discover_Regenerative/can_be_enabled_and_disabled.yml diff --git a/spec/lib/tasks/enterprises_rake_spec.rb b/spec/lib/tasks/enterprises_rake_spec.rb index 0ebf97b7d7..2e98502f0e 100644 --- a/spec/lib/tasks/enterprises_rake_spec.rb +++ b/spec/lib/tasks/enterprises_rake_spec.rb @@ -20,4 +20,26 @@ RSpec.describe 'enterprises.rake' do end end end + + describe ':enterprises' do + describe ':activate_connected_app_type' do + it 'updates only disconnected enterprises' do + # enterprise with affiliate sales data + enterprise_asd = create(:enterprise) + enterprise_asd.connected_apps.create type: 'ConnectedApps::AffiliateSalesData' + # enterprise with different type + enterprise_diff = create(:enterprise) + enterprise_diff.connected_apps.create + + expect { + Rake.application.invoke_task( + "ofn:enterprises:activate_connected_app_type[affiliate_sales_data]" + ) + }.to change { ConnectedApps::AffiliateSalesData.count }.by(1) + + expect(enterprise_asd.connected_apps.affiliate_sales_data.count).to eq 1 + expect(enterprise_diff.connected_apps.affiliate_sales_data.count).to eq 1 + end + end + end end diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index c53b8d7845..a2a0ab3897 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -268,4 +268,34 @@ RSpec.describe Spree::User do expect(user.disabled).to be_falsey end end + + describe "#affiliate_enterprises" do + let(:user) { create(:user) } + let(:affiliate_enterprise) { create(:enterprise) } + let(:other_connected_enterprise) { create(:enterprise) } + let(:other_enterprise) { create(:enterprise) } + subject{ user.affiliate_enterprises } + + before do + ConnectedApps::AffiliateSalesData.create(enterprise: affiliate_enterprise, data: true) + ConnectedApp.create(enterprise: other_connected_enterprise, data: true) + end + + context "user does not have feature" do + it { is_expected.to be_empty } + end + + context "user has feature affiliate_sales_data" do + before do + Flipper.enable_actor(:affiliate_sales_data, user) + user.reload + end + + it "includes only affiliate enterprises" do + is_expected.to include affiliate_enterprise + is_expected.not_to include other_connected_enterprise + is_expected.not_to include other_enterprise + end + end + end end diff --git a/spec/system/admin/enterprises/connected_apps_spec.rb b/spec/system/admin/enterprises/connected_apps_spec.rb index 652982b0ed..4f1a67b3f7 100644 --- a/spec/system/admin/enterprises/connected_apps_spec.rb +++ b/spec/system/admin/enterprises/connected_apps_spec.rb @@ -28,36 +28,91 @@ RSpec.describe "Connected Apps", feature: :connected_apps, vcr: true do expect(page).to have_content "CONNECTED APPS" end - it "can be enabled and disabled" do - visit edit_admin_enterprise_path(enterprise) + describe "Discover Regenerative" do + let(:section_heading) { self.class.description } - scroll_to :bottom - click_link "Connected apps" - expect(page).to have_content "Discover Regenerative" + it "can be enabled and disabled" do + visit edit_admin_enterprise_path(enterprise) - click_button "Allow data sharing" - expect(page).not_to have_button "Allow data sharing" - expect(page).to have_button "Loading", disabled: true + scroll_to :bottom + click_link "Connected apps" - perform_enqueued_jobs(only: ConnectAppJob) - expect(page).not_to have_button "Loading", disabled: true - expect(page).to have_content "account is connected" - expect(page).to have_link "Manage listing" + within section_containing_heading do + click_button "Allow data sharing" + end - click_button "Stop sharing" - expect(page).to have_button "Allow data sharing" - expect(page).not_to have_button "Stop sharing" - expect(page).not_to have_content "account is connected" - expect(page).not_to have_link "Manage listing" + # (page is reloaded so we need to evaluate within block again) + within section_containing_heading do + expect(page).not_to have_button "Allow data sharing" + expect(page).to have_button "Loading", disabled: true + + perform_enqueued_jobs(only: ConnectAppJob) + + expect(page).not_to have_button "Loading", disabled: true + expect(page).to have_content "account is connected" + expect(page).to have_link "Manage listing" + + click_button "Stop sharing" + end + + within section_containing_heading do + expect(page).to have_button "Allow data sharing" + expect(page).not_to have_button "Stop sharing" + expect(page).not_to have_content "account is connected" + expect(page).not_to have_link "Manage listing" + end + end + + it "can't be enabled by non-manager" do + login_as create(:admin_user) + + visit "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel" + + within section_containing_heading do + expect(page).to have_button("Allow data sharing", disabled: true) + expect(page).to have_content "Only managers can connect apps." + end + end end - it "can't be enabled by non-manager" do - login_as create(:admin_user) + describe "Affiliate Sales Data" do + let(:section_heading) { "INRAE / UFC QUE CHOISIR Research" } - visit "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel" - expect(page).to have_content "Discover Regenerative" + it "can be enabled and disabled" do + visit edit_admin_enterprise_path(enterprise) - expect(page).to have_button("Allow data sharing", disabled: true) - expect(page).to have_content "Only managers can connect apps." + scroll_to :bottom + click_link "Connected apps" + + within section_containing_heading do + click_button "Allow data sharing" + end + + # (page is reloaded so we need to evaluate within block again) + within section_containing_heading do + expect(page).not_to have_button "Allow data sharing" + click_button "Stop sharing" + end + + within section_containing_heading do + expect(page).to have_button "Allow data sharing" + expect(page).not_to have_button "Stop sharing" + end + end + + it "can't be enabled by non-manager" do + login_as create(:admin_user) + + visit "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel" + + within section_containing_heading do + expect(page).to have_button("Allow data sharing", disabled: true) + expect(page).to have_content "Only managers can connect apps." + end + end + end + + def section_containing_heading(heading = section_heading) + page.find("h3", text: heading).ancestor("section") end end