Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Affiliate Sales Data" connected app option #12676

22 changes: 10 additions & 12 deletions app/controllers/admin/connected_apps_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,22 @@ class ConnectedAppsController < ApplicationController
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

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

Expand All @@ -39,5 +33,9 @@ def enterprise
def render_panel
redirect_to "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel"
end

def connected_app_params
params.permit(:type)
end
end
end
4 changes: 2 additions & 2 deletions app/jobs/connect_app_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ def perform(app, token, channel: nil)

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 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that the more generic alternative would be ConnectedApp.of(enterprise).first. We'll see with more usage what will be better.

)

cable_ready[channel].morph(selector:, html:).broadcast
Expand Down
24 changes: 24 additions & 0 deletions app/models/connected_app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +24 to +34
Copy link
Collaborator

@rioug rioug Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that be better in a service ?
It made more sense now that I have seen the rest of the commits. I am still not a fan but right now I can't really think of another solution without bringing more complexity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it feels wrong sending the websocket channel to the model. But with a service, I guess the controller has to know about the subtypes, and make the decision what to do with them, which doesn't seem any better.
I'm curious what Maikel thinks too.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean you could have a service that knows how to connect/disconnect each type of connected app, but that would add some indirection.
It could make sense if the connect/disconnect was a setting not stored with the model.

end
13 changes: 13 additions & 0 deletions app/models/connected_apps/affiliate_sales_data.rb
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +7 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we set this value in the constructor so that we don't have to update the record again? Doesn't really matter though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thinking


def disconnect; end
end
end
6 changes: 6 additions & 0 deletions app/models/spree/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ def disabled=(value)
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?
Expand Down
34 changes: 4 additions & 30 deletions app/views/admin/enterprises/form/_connected_apps.html.haml
Original file line number Diff line number Diff line change
@@ -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
&nbsp;
= 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 }
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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
&nbsp;
= 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"
10 changes: 9 additions & 1 deletion app/webpacker/css/admin/connected_apps.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,23 @@ en:
custom_tab_content: "Content for custom tab"
connected_apps:
legend: "Connected apps"
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: |
<p>
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.
</p>
<p>
<a href="https://apropos.coopcircuits.fr/"
target="_blank"><b>Learn more about this research project</b>
<i class="icon-external-link"></i></a>
</p>
discover_regen:
title: "Discover Regenerative"
tagline: "Allow Discover Regenerative to publish your enterprise information."
enable: "Allow data sharing"
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20240715072649_add_type_to_connected_apps.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddTypeToConnectedApps < ActiveRecord::Migration[7.0]
def change
add_column :connected_apps, :type, :string, default: "ConnectedApp", null: false
end
end
1 change: 1 addition & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
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

Expand Down
12 changes: 12 additions & 0 deletions lib/tasks/enterprises.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we should whitelist the type param here ? Probably not necessary as it will blow up if you are trying to use a scope that doesn't exist.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I figured for a rake task that requires system access to execute, we don't need to add much protection.
Maybe I should have hardcoded the type (there's only one type after all!).


"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
Expand Down
22 changes: 22 additions & 0 deletions spec/lib/tasks/enterprises_rake_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,26 @@
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
30 changes: 30 additions & 0 deletions spec/models/spree/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,34 @@
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
Loading
Loading