DEV: adds an automation script for first accepted solution (#172)

DEV: adds an automation script for first accepted solution (#172)

diff --git a/app/lib/first_accepted_post_solution_validator.rb b/app/lib/first_accepted_post_solution_validator.rb
new file mode 100644
index 0000000..898e213
--- /dev/null
+++ b/app/lib/first_accepted_post_solution_validator.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class FirstAcceptedPostSolutionValidator
+  def self.check(post, trust_level:)
+    return false if post.archetype != Archetype.default
+    return false if !post&.user&.human?
+    return true if trust_level == 'any'
+
+    if TrustLevel.compare(post&.user&.trust_level, trust_level.to_i)
+      return false
+    end
+
+    if !UserAction.where(user_id: post&.user_id, action_type: UserAction::SOLVED).exists?
+      return true
+    end
+
+    false
+  end
+end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 9dd018e..90b548b 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -42,3 +42,17 @@ en:
         solved_event:
           name: "Solved Event"
           details: "When a user marks a post as the accepted or unaccepted answer."
+
+    discourse_automation:
+      triggerables:
+        first_accepted_solution:
+          max_trust_level:
+            tl1: < TL1
+            tl2: < TL2
+            tl3: < TL3
+            tl4: < TL4
+            any: Any
+          fields:
+            maximum_trust_level:
+              label: Trust Level
+              description: Users under this Trust Level will trigger this automation
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index cb635f4..18b9e5b 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -33,6 +33,12 @@ en:
       name: "Tech Support"
       description: "10 Accepted answers"
 
+  discourse_automation:
+    triggerables:
+      first_accepted_solution:
+        title: First accepted solution
+        doc: Triggers when a user got a solution accepted for the first time.
+
   education:
     topic_is_solved: |
       ### This topic has been solved
diff --git a/plugin.rb b/plugin.rb
index 1af9bec..c4557c7 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -24,6 +24,7 @@ after_initialize do
   SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-solved", "db", "fixtures").to_s
 
   [
+    '../app/lib/first_accepted_post_solution_validator.rb',
     '../app/serializers/concerns/topic_answer_mixin.rb'
   ].each { |path| load File.expand_path(path, __FILE__) }
 
@@ -752,4 +753,45 @@ SQL
       prepend AddSolvedToTopicPostersSummary
     end
   end
+
+  if defined?(DiscourseAutomation)
+    if respond_to?(:add_triggerable_to_scriptable)
+      on(:accepted_solution) do |post|
+        # testing directly automation is prone to issues
+        # we prefer to abstract logic in service object and test this
+        next if Rails.env.test?
+
+        name = 'first_accepted_solution'
+        DiscourseAutomation::Automation.where(trigger: name, enabled: true).find_each do |automation|
+          maximum_trust_level = automation.trigger_field('maximum_trust_level')&.dig('value')
+          if FirstAcceptedPostSolutionValidator.check(post, trust_level: maximum_trust_level)
+            automation.trigger!(
+              'kind' => name,
+              'accepted_post_id' => post.id,
+              'usernames' => [post.user.username],
+              'placeholders' => {
+                'post_url' => Discourse.base_url + post.url
+              }
+            )
+          end
+        end
+      end
+
+      TRUST_LEVELS = [
+        { id: 1, name: 'discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl1' },
+        { id: 2, name: 'discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl2' },
+        { id: 3, name: 'discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl3' },
+        { id: 4, name: 'discourse_automation.triggerables.first_accepted_solution.max_trust_level.tl4' },
+        { id: 'any', name: 'discourse_automation.triggerables.first_accepted_solution.max_trust_level.any' },
+      ]
+
+      add_triggerable_to_scriptable(:first_accepted_solution, :send_pms)
+
+      DiscourseAutomation::Triggerable.add(:first_accepted_solution) do
+        placeholder :post_url
+
+        field :maximum_trust_level, component: :choices, extra: { content: TRUST_LEVELS }, required: true
+      end
+    end
+  end
 end
diff --git a/spec/lib/first_accepted_post_solution_validator_spec.rb b/spec/lib/first_accepted_post_solution_validator_spec.rb
new file mode 100644
index 0000000..39d07b3
--- /dev/null
+++ b/spec/lib/first_accepted_post_solution_validator_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe FirstAcceptedPostSolutionValidator do
+  fab!(:user_tl1) { Fabricate(:user, trust_level: TrustLevel[1]) }
+
+  context 'user is under max trust level' do
+    context 'has no post accepted yet' do
+      it 'validates the post' do
+        post_1 = create_post(user: user_tl1)
+        expect(described_class.check(post_1, trust_level: TrustLevel[2])).to eq(true)
+      end
+    end
+
+    context 'has already had accepted posts' do
+      before do
+        accepted_post = create_post(user: user_tl1)
+        DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user)
+      end
+
+      it 'doesn’t validate the post' do
+        post_1 = create_post(user: user_tl1)
+        expect(described_class.check(post_1, trust_level: TrustLevel[2])).to eq(false)
+      end
+    end
+  end
+
+  context 'user is above or equal max trust level' do
+    context 'has no post accepted yet' do
+      it 'doesn’t validate the post' do
+        post_1 = create_post(user: user_tl1)
+        expect(described_class.check(post_1, trust_level: TrustLevel[1])).to eq(false)
+      end
+    end
+
+    context 'has already had accepted posts' do
+      before do
+        accepted_post = create_post(user: user_tl1)
+        DiscourseSolved.accept_answer!(accepted_post, Discourse.system_user)
+      end
+
+      it 'doesn’t validate the post' do
+        post_1 = create_post(user: user_tl1)
+        expect(described_class.check(post_1, trust_level: TrustLevel[1])).to eq(false)
+      end
+    end
+  end
+
+  context 'using any trust level' do
+    it 'validates the post' do
+      post_1 = create_post(user: user_tl1)
+      expect(described_class.check(post_1, trust_level: 'any')).to eq(true)
+    end
+  end
+
+  context 'user is system' do
+    it 'doesn’t validate the post' do
+      post_1 = create_post(user: Discourse.system_user)
+      expect(described_class.check(post_1, trust_level: 'any')).to eq(false)
+    end
+  end
+
+  context 'post is a PM' do
+    it 'doesn’t validate the post' do
+      post_1 = create_post(user: user_tl1, target_usernames: [user_tl1.username], archetype: Archetype.private_message)
+      expect(described_class.check(post_1, trust_level: 'any')).to eq(false)
+    end
+  end
+end

GitHub sha: 9b8aa0985515c09adeeb85732b75ff62ced3b9ea

This commit appears in #172 which was merged by jjaffeux.