From 63d38ad0822753e352e0d207570ab82aae5b6ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Mon, 30 Sep 2024 14:42:37 -0300 Subject: [PATCH 01/17] feat: add questing domain --- apps/govquests-api/govquests/configuration.rb | 2 + .../govquests/questing/.mutant.yml | 11 ++ apps/govquests-api/govquests/questing/Gemfile | 4 + .../govquests/questing/Gemfile.lock | 110 ++++++++++++++++++ .../govquests-api/govquests/questing/Makefile | 10 ++ .../govquests/questing/README.md | 7 ++ .../govquests/questing/lib/questing.rb | 13 +++ .../lib/questing/commands/register_quest.rb | 7 ++ .../lib/questing/events/quest_registered.rb | 6 + .../govquests/questing/lib/questing/quest.rb | 23 ++++ .../questing/lib/questing/quest_service.rb | 13 +++ .../questing/test/register_quest_test.rb | 32 +++++ .../govquests/questing/test/test_helper.rb | 19 +++ .../app/read_models/quests/configuration.rb | 11 ++ .../app/read_models/quests/create_quest.rb | 8 ++ .../migrate/20240930172742_create_quests.rb | 9 ++ apps/govquests-api/rails_app/db/schema.rb | 8 +- .../rails_app/lib/configuration.rb | 5 + .../rails_app/test/quests/create_test.rb | 32 +++++ 19 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 apps/govquests-api/govquests/questing/.mutant.yml create mode 100644 apps/govquests-api/govquests/questing/Gemfile create mode 100644 apps/govquests-api/govquests/questing/Gemfile.lock create mode 100644 apps/govquests-api/govquests/questing/Makefile create mode 100644 apps/govquests-api/govquests/questing/README.md create mode 100644 apps/govquests-api/govquests/questing/lib/questing.rb create mode 100644 apps/govquests-api/govquests/questing/lib/questing/commands/register_quest.rb create mode 100644 apps/govquests-api/govquests/questing/lib/questing/events/quest_registered.rb create mode 100644 apps/govquests-api/govquests/questing/lib/questing/quest.rb create mode 100644 apps/govquests-api/govquests/questing/lib/questing/quest_service.rb create mode 100644 apps/govquests-api/govquests/questing/test/register_quest_test.rb create mode 100644 apps/govquests-api/govquests/questing/test/test_helper.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/quests/configuration.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/quests/create_quest.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20240930172742_create_quests.rb create mode 100644 apps/govquests-api/rails_app/test/quests/create_test.rb diff --git a/apps/govquests-api/govquests/configuration.rb b/apps/govquests-api/govquests/configuration.rb index 81c7641..7d2b760 100644 --- a/apps/govquests-api/govquests/configuration.rb +++ b/apps/govquests-api/govquests/configuration.rb @@ -1,4 +1,5 @@ require_relative "authentication/lib/authentication" +require_relative "questing/lib/questing" module Govquests @@ -12,6 +13,7 @@ def call(event_store, command_bus) def configure_bounded_contexts(event_store, command_bus) [ Authentication::Configuration.new, + Questing::Configuration.new, ].each { |c| c.call(event_store, command_bus) } end diff --git a/apps/govquests-api/govquests/questing/.mutant.yml b/apps/govquests-api/govquests/questing/.mutant.yml new file mode 100644 index 0000000..a4ea1c5 --- /dev/null +++ b/apps/govquests-api/govquests/questing/.mutant.yml @@ -0,0 +1,11 @@ +requires: + - ./test/test_helper +integration: minitest +usage: opensource +coverage_criteria: + process_abort: true +matcher: + subjects: + - Questing* + ignore: + - Questing::Configuration#call diff --git a/apps/govquests-api/govquests/questing/Gemfile b/apps/govquests-api/govquests/questing/Gemfile new file mode 100644 index 0000000..00fef6d --- /dev/null +++ b/apps/govquests-api/govquests/questing/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +eval_gemfile "../../infra/Gemfile.test" +gem "infra", path: "../../infra" diff --git a/apps/govquests-api/govquests/questing/Gemfile.lock b/apps/govquests-api/govquests/questing/Gemfile.lock new file mode 100644 index 0000000..d3c8cdd --- /dev/null +++ b/apps/govquests-api/govquests/questing/Gemfile.lock @@ -0,0 +1,110 @@ +PATH + remote: ../../infra + specs: + infra (1.0.0) + aggregate_root (~> 2.15) + arkency-command_bus + dry-struct + dry-types + rake + ruby_event_store (~> 2.15) + ruby_event_store-transformations + +GEM + remote: https://oss:7AXfeZdAfCqL1PvHm2nvDJO6Zd9UW8IK@gem.mutant.dev/ + specs: + mutant-license (0.1.1.2.1627430819213747598431630701693729869473.6) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + aggregate_root (2.15.0) + ruby_event_store (= 2.15.0) + arkency-command_bus (0.4.1) + concurrent-ruby + ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.4) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + diff-lcs (1.5.1) + drb (2.2.0) + ruby2_keywords + dry-core (1.0.1) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.7.1) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + minitest (5.25.0) + mutant (0.12.4) + diff-lcs (~> 1.3) + parser (~> 3.3.0) + regexp_parser (~> 2.9.0) + sorbet-runtime (~> 0.5.0) + unparser (~> 0.6.14) + mutant-minitest (0.12.4) + minitest (~> 5.11) + mutant (= 0.12.4) + mutex_m (0.2.0) + parser (3.3.4.2) + ast (~> 2.4.1) + racc + racc (1.8.1) + rake (13.1.0) + regexp_parser (2.9.2) + ruby2_keywords (0.0.5) + ruby_event_store (2.15.0) + concurrent-ruby (~> 1.0, >= 1.1.6) + ruby_event_store-transformations (0.1.0) + activesupport (>= 5.0) + ruby_event_store (>= 2.0.0, < 3.0.0) + sorbet-runtime (0.5.11525) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unparser (0.6.15) + diff-lcs (~> 1.3) + parser (>= 3.3.0) + zeitwerk (2.6.12) + +PLATFORMS + arm64-darwin-20 + arm64-darwin-21 + ruby + x86_64-darwin-20 + x86_64-linux + +DEPENDENCIES + infra! + minitest (= 5.25.0)! + mutant-license! + mutant-minitest (= 0.12.4)! + +BUNDLED WITH + 2.5.9 diff --git a/apps/govquests-api/govquests/questing/Makefile b/apps/govquests-api/govquests/questing/Makefile new file mode 100644 index 0000000..23850fa --- /dev/null +++ b/apps/govquests-api/govquests/questing/Makefile @@ -0,0 +1,10 @@ +install: + @bundle install + +test: + @bundle exec ruby -e "require \"rake/rake_test_loader\"" test/*_test.rb + +mutate: + @RAILS_ENV=test bundle exec mutant run + +.PHONY: install test mutate diff --git a/apps/govquests-api/govquests/questing/README.md b/apps/govquests-api/govquests/questing/README.md new file mode 100644 index 0000000..b187021 --- /dev/null +++ b/apps/govquests-api/govquests/questing/README.md @@ -0,0 +1,7 @@ +# Questing + +#### Up and running + +``` +make install test mutate +``` diff --git a/apps/govquests-api/govquests/questing/lib/questing.rb b/apps/govquests-api/govquests/questing/lib/questing.rb new file mode 100644 index 0000000..116303f --- /dev/null +++ b/apps/govquests-api/govquests/questing/lib/questing.rb @@ -0,0 +1,13 @@ +require "infra" +require_relative "questing/commands/register_quest" +require_relative "questing/events/quest_registered" +require_relative "questing/quest_service" +require_relative "questing/quest" + +module Questing + class Configuration + def call(event_store, command_bus) + command_bus.register(RegisterQuest, RegisterQuestHandler.new(event_store)) + end + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands/register_quest.rb b/apps/govquests-api/govquests/questing/lib/questing/commands/register_quest.rb new file mode 100644 index 0000000..f12da69 --- /dev/null +++ b/apps/govquests-api/govquests/questing/lib/questing/commands/register_quest.rb @@ -0,0 +1,7 @@ +module Questing + class RegisterQuest < Infra::Command + attribute :quest_id, Infra::Types::UUID + + alias aggregate_id quest_id + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing/events/quest_registered.rb b/apps/govquests-api/govquests/questing/lib/questing/events/quest_registered.rb new file mode 100644 index 0000000..6ca2317 --- /dev/null +++ b/apps/govquests-api/govquests/questing/lib/questing/events/quest_registered.rb @@ -0,0 +1,6 @@ +module Questing + class QuestRegistered < Infra::Event + attribute :quest_id, Infra::Types::UUID + + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest.rb b/apps/govquests-api/govquests/questing/lib/questing/quest.rb new file mode 100644 index 0000000..75fa260 --- /dev/null +++ b/apps/govquests-api/govquests/questing/lib/questing/quest.rb @@ -0,0 +1,23 @@ +module Questing + class Quest + include AggregateRoot + + AlreadyRegistered = Class.new(StandardError) + + def initialize(id) + @id = id + end + + def register + raise AlreadyRegistered if @registered + + apply QuestRegistered.new(data: { quest_id: @id }) + end + + private + + on QuestRegistered do |event| + @registered = true + end + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest_service.rb b/apps/govquests-api/govquests/questing/lib/questing/quest_service.rb new file mode 100644 index 0000000..00ef372 --- /dev/null +++ b/apps/govquests-api/govquests/questing/lib/questing/quest_service.rb @@ -0,0 +1,13 @@ +module Questing + class RegisterQuestHandler + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + @repository.with_aggregate(Quest, command.aggregate_id) do |quest| + quest.register + end + end + end +end diff --git a/apps/govquests-api/govquests/questing/test/register_quest_test.rb b/apps/govquests-api/govquests/questing/test/register_quest_test.rb new file mode 100644 index 0000000..0c4bd2b --- /dev/null +++ b/apps/govquests-api/govquests/questing/test/register_quest_test.rb @@ -0,0 +1,32 @@ +require_relative "test_helper" + +module Questing + class RegisterQuestTest < Test + cover "Questing*" + + def setup + @uid = SecureRandom.uuid + @data = { quest_id: @uid, } + end + + def test_quest_should_get_registered + quest_registered = QuestRegistered.new(data: @data) + assert_events("Questing::Quest$#{@uid}", quest_registered) do + register_quest(@uid) + end + end + + def test_should_not_allow_for_double_registration + assert_raises(Quest::AlreadyRegistered) do + register_quest(@uid) + register_quest(@uid) + end + end + + private + + def register_quest(quest_id) + run_command(RegisterQuest.new(quest_id: quest_id)) + end + end +end diff --git a/apps/govquests-api/govquests/questing/test/test_helper.rb b/apps/govquests-api/govquests/questing/test/test_helper.rb new file mode 100644 index 0000000..e77a85b --- /dev/null +++ b/apps/govquests-api/govquests/questing/test/test_helper.rb @@ -0,0 +1,19 @@ +require "minitest/autorun" +require "mutant/minitest/coverage" + +require_relative "../lib/questing" + +module Questing + class Test < Infra::InMemoryTest + def before_setup + super() + Configuration.new.call(event_store, command_bus) + end + + private + + def fake_login + "fake_login" + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/quests/configuration.rb b/apps/govquests-api/rails_app/app/read_models/quests/configuration.rb new file mode 100644 index 0000000..d44a769 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/quests/configuration.rb @@ -0,0 +1,11 @@ +module Quests + class Quest < ApplicationRecord + self.table_name = "quests" + end + + class Configuration + def call(event_store) + event_store.subscribe(CreateQuest, to: [ Questing::QuestRegistered ]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/quests/create_quest.rb b/apps/govquests-api/rails_app/app/read_models/quests/create_quest.rb new file mode 100644 index 0000000..c587562 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/quests/create_quest.rb @@ -0,0 +1,8 @@ +module Quests + class CreateQuest + def call(event) + quest_id = event.data.fetch(:quest_id) + Quest.find_or_create_by(quest_id: quest_id) + end + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930172742_create_quests.rb b/apps/govquests-api/rails_app/db/migrate/20240930172742_create_quests.rb new file mode 100644 index 0000000..233140a --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20240930172742_create_quests.rb @@ -0,0 +1,9 @@ +class CreateQuests < ActiveRecord::Migration[8.0] + def change + create_table :quests do |t| + t.string :quest_id + + t.timestamps + end + end +end diff --git a/apps/govquests-api/rails_app/db/schema.rb b/apps/govquests-api/rails_app/db/schema.rb index 4993a9e..4804a7c 100644 --- a/apps/govquests-api/rails_app/db/schema.rb +++ b/apps/govquests-api/rails_app/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_09_27_211319) do +ActiveRecord::Schema[8.0].define(version: 2024_09_30_172742) do create_table "accounts", force: :cascade do |t| t.string "account_id" t.string "string" @@ -44,5 +44,11 @@ t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true end + create_table "quests", force: :cascade do |t| + t.string "quest_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + add_foreign_key "event_store_events_in_streams", "event_store_events", column: "event_id", primary_key: "event_id" end diff --git a/apps/govquests-api/rails_app/lib/configuration.rb b/apps/govquests-api/rails_app/lib/configuration.rb index 422ad57..8e7d40d 100644 --- a/apps/govquests-api/rails_app/lib/configuration.rb +++ b/apps/govquests-api/rails_app/lib/configuration.rb @@ -6,6 +6,7 @@ def call(event_store, command_bus) enable_res_infra_event_linking(event_store) enable_authentication_read_model(event_store) + enable_quests_read_model(event_store) Govquests::Configuration.new.call(event_store, command_bus) end @@ -17,6 +18,10 @@ def enable_authentication_read_model(event_store) ClientAuthentication::Configuration.new.call(event_store) end + def enable_quests_read_model(event_store) + Quests::Configuration.new.call(event_store) + end + def enable_res_infra_event_linking(event_store) [ diff --git a/apps/govquests-api/rails_app/test/quests/create_test.rb b/apps/govquests-api/rails_app/test/quests/create_test.rb new file mode 100644 index 0000000..88d17d9 --- /dev/null +++ b/apps/govquests-api/rails_app/test/quests/create_test.rb @@ -0,0 +1,32 @@ +require "test_helper" + +module Quests + class CreateTest < InMemoryTestCase + cover "Questing*" + + def setup + super + Quests::Quest.destroy_all + end + + def test_set_create + quest_id = SecureRandom.uuid + + + run_command( + Questing::RegisterQuest.new( + quest_id: quest_id, + ) + ) + + account = Quests::Quest.find_by(quest_id: quest_id) + assert account.present? + end + + private + + def event_store + Rails.configuration.event_store + end + end +end From 88c0626134b959f3bda192268a44a6acc338bda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Mon, 30 Sep 2024 15:00:42 -0300 Subject: [PATCH 02/17] refactor: simplify folder domain structure --- .../govquests/authentication/lib/authentication.rb | 2 +- .../{commands/register_account.rb => commands.rb} | 0 .../authentication/{events/account_registered.rb => events.rb} | 0 .../{account_service.rb => on_register_account.rb} | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename apps/govquests-api/govquests/authentication/lib/authentication/{commands/register_account.rb => commands.rb} (100%) rename apps/govquests-api/govquests/authentication/lib/authentication/{events/account_registered.rb => events.rb} (100%) rename apps/govquests-api/govquests/authentication/lib/authentication/{account_service.rb => on_register_account.rb} (91%) diff --git a/apps/govquests-api/govquests/authentication/lib/authentication.rb b/apps/govquests-api/govquests/authentication/lib/authentication.rb index 28e54d8..42dfeb5 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication.rb @@ -7,7 +7,7 @@ module Authentication class Configuration def call(event_store, command_bus) - command_bus.register(RegisterAccount, RegisterAccountHandler.new(event_store)) + command_bus.register(RegisterAccount, OnRegisterAccount.new(event_store)) end end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/commands/register_account.rb b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb similarity index 100% rename from apps/govquests-api/govquests/authentication/lib/authentication/commands/register_account.rb rename to apps/govquests-api/govquests/authentication/lib/authentication/commands.rb diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/events/account_registered.rb b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb similarity index 100% rename from apps/govquests-api/govquests/authentication/lib/authentication/events/account_registered.rb rename to apps/govquests-api/govquests/authentication/lib/authentication/events.rb diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/account_service.rb b/apps/govquests-api/govquests/authentication/lib/authentication/on_register_account.rb similarity index 91% rename from apps/govquests-api/govquests/authentication/lib/authentication/account_service.rb rename to apps/govquests-api/govquests/authentication/lib/authentication/on_register_account.rb index 046bac5..fe81537 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/account_service.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/on_register_account.rb @@ -1,5 +1,5 @@ module Authentication - class RegisterAccountHandler + class OnRegisterAccount def initialize(event_store) @repository = Infra::AggregateRootRepository.new(event_store) end From ec4075388577a975d37e2f4c1d8377924e98b505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Mon, 30 Sep 2024 15:19:29 -0300 Subject: [PATCH 03/17] refactor: simplify questing domain --- apps/govquests-api/govquests/questing/lib/questing.rb | 2 +- .../govquests/questing/lib/questing/commands.rb | 7 +++++++ .../questing/lib/questing/commands/register_quest.rb | 7 ------- .../lib/questing/{events/quest_registered.rb => events.rb} | 4 ++-- .../lib/questing/{quest_service.rb => on_quest_created.rb} | 5 +++-- .../govquests-api/govquests/questing/lib/questing/quest.rb | 5 +++-- 6 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 apps/govquests-api/govquests/questing/lib/questing/commands.rb delete mode 100644 apps/govquests-api/govquests/questing/lib/questing/commands/register_quest.rb rename apps/govquests-api/govquests/questing/lib/questing/{events/quest_registered.rb => events.rb} (53%) rename apps/govquests-api/govquests/questing/lib/questing/{quest_service.rb => on_quest_created.rb} (62%) diff --git a/apps/govquests-api/govquests/questing/lib/questing.rb b/apps/govquests-api/govquests/questing/lib/questing.rb index 116303f..c514fc6 100644 --- a/apps/govquests-api/govquests/questing/lib/questing.rb +++ b/apps/govquests-api/govquests/questing/lib/questing.rb @@ -7,7 +7,7 @@ module Questing class Configuration def call(event_store, command_bus) - command_bus.register(RegisterQuest, RegisterQuestHandler.new(event_store)) + command_bus.register(RegisterQuest, OnQuestCreated.new(event_store)) end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands.rb b/apps/govquests-api/govquests/questing/lib/questing/commands.rb new file mode 100644 index 0000000..ab430ac --- /dev/null +++ b/apps/govquests-api/govquests/questing/lib/questing/commands.rb @@ -0,0 +1,7 @@ +module Questing + class CreateQuest < Infra::Command + attribute :quest_id, Infra::Types::UUID + + alias_method :aggregate_id, :quest_id + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands/register_quest.rb b/apps/govquests-api/govquests/questing/lib/questing/commands/register_quest.rb deleted file mode 100644 index f12da69..0000000 --- a/apps/govquests-api/govquests/questing/lib/questing/commands/register_quest.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Questing - class RegisterQuest < Infra::Command - attribute :quest_id, Infra::Types::UUID - - alias aggregate_id quest_id - end -end diff --git a/apps/govquests-api/govquests/questing/lib/questing/events/quest_registered.rb b/apps/govquests-api/govquests/questing/lib/questing/events.rb similarity index 53% rename from apps/govquests-api/govquests/questing/lib/questing/events/quest_registered.rb rename to apps/govquests-api/govquests/questing/lib/questing/events.rb index 6ca2317..40b8b6c 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/events/quest_registered.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/events.rb @@ -1,6 +1,6 @@ +# lib/questing/events.rb module Questing - class QuestRegistered < Infra::Event + class QuestCreated < Infra::Event attribute :quest_id, Infra::Types::UUID - end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest_service.rb b/apps/govquests-api/govquests/questing/lib/questing/on_quest_created.rb similarity index 62% rename from apps/govquests-api/govquests/questing/lib/questing/quest_service.rb rename to apps/govquests-api/govquests/questing/lib/questing/on_quest_created.rb index 00ef372..8f83cc9 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/quest_service.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/on_quest_created.rb @@ -1,12 +1,13 @@ +# lib/questing/on_register_quest.rb module Questing - class RegisterQuestHandler + class OnQuestCreated def initialize(event_store) @repository = Infra::AggregateRootRepository.new(event_store) end def call(command) @repository.with_aggregate(Quest, command.aggregate_id) do |quest| - quest.register + quest.create(command.audience, command.type, command.duration, command.difficulty) end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest.rb b/apps/govquests-api/govquests/questing/lib/questing/quest.rb index 75fa260..c33e2ce 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/quest.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/quest.rb @@ -11,13 +11,14 @@ def initialize(id) def register raise AlreadyRegistered if @registered - apply QuestRegistered.new(data: { quest_id: @id }) + apply QuestCreated.new(data: {quest_id: @id}) end private - on QuestRegistered do |event| + on QuestCreated do |event| @registered = true + @status = :draft end end end From 2fba0559fbb1320b205f468f91f83fe1143e4dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Wed, 2 Oct 2024 16:22:45 -0300 Subject: [PATCH 04/17] feat: add questing domain --- .../govquests/questing/lib/questing.rb | 11 ++-- .../questing/lib/questing/commands.rb | 21 +++++++ .../govquests/questing/lib/questing/events.rb | 23 +++++++- .../lib/questing/on_quest_commands.rb | 28 +++++++++ .../questing/lib/questing/on_quest_created.rb | 14 ----- .../govquests/questing/lib/questing/quest.rb | 58 ++++++++++++++++--- .../questing/lib/questing/value_objects.rb | 27 +++++++++ .../questing/test/register_quest_test.rb | 6 +- .../app/read_models/questing/configuration.rb | 11 ++++ .../app/read_models/questing/create_quest.rb | 26 +++++++++ .../questing/update_quest_status.rb | 11 ++++ .../migrate/20240930201726_create_quests.rb | 16 +++++ 12 files changed, 223 insertions(+), 29 deletions(-) create mode 100644 apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb delete mode 100644 apps/govquests-api/govquests/questing/lib/questing/on_quest_created.rb create mode 100644 apps/govquests-api/govquests/questing/lib/questing/value_objects.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/questing/configuration.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/questing/create_quest.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/questing/update_quest_status.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb diff --git a/apps/govquests-api/govquests/questing/lib/questing.rb b/apps/govquests-api/govquests/questing/lib/questing.rb index c514fc6..91832cb 100644 --- a/apps/govquests-api/govquests/questing/lib/questing.rb +++ b/apps/govquests-api/govquests/questing/lib/questing.rb @@ -1,13 +1,16 @@ require "infra" -require_relative "questing/commands/register_quest" -require_relative "questing/events/quest_registered" -require_relative "questing/quest_service" +require_relative "questing/commands" +require_relative "questing/events" +require_relative "questing/on_quest_commands" require_relative "questing/quest" +# app/read_models/quests/configuration.rb module Questing class Configuration def call(event_store, command_bus) - command_bus.register(RegisterQuest, OnQuestCreated.new(event_store)) + event_store.subscribe(CreateQuest, to: [Questing::QuestCreated]) + event_store.subscribe(UpdateQuestStatus, to: [Questing::QuestProgressUpdated]) + # Add additional subscriptions as needed end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands.rb b/apps/govquests-api/govquests/questing/lib/questing/commands.rb index ab430ac..7dde1ae 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/commands.rb @@ -1,6 +1,27 @@ module Questing class CreateQuest < Infra::Command attribute :quest_id, Infra::Types::UUID + attribute :audience, Infra::Types::String + attribute :type, Infra::Types::String + attribute :duration, Infra::Types::Integer + attribute :difficulty, Infra::Types::String + attribute :requirements, Infra::Types::Array.optional + attribute :reward, Infra::Types::Hash.optional + + alias_method :aggregate_id, :quest_id + end + + class AssociateActionWithQuest < Infra::Command + attribute :quest_id, Infra::Types::UUID + attribute :action_id, Infra::Types::UUID + + alias_method :aggregate_id, :quest_id + end + + class UpdateQuestProgress < Infra::Command + attribute :quest_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + attribute :action_id, Infra::Types::UUID alias_method :aggregate_id, :quest_id end diff --git a/apps/govquests-api/govquests/questing/lib/questing/events.rb b/apps/govquests-api/govquests/questing/lib/questing/events.rb index 40b8b6c..a2cb4f4 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/events.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/events.rb @@ -1,6 +1,27 @@ -# lib/questing/events.rb module Questing class QuestCreated < Infra::Event attribute :quest_id, Infra::Types::UUID + attribute :audience, Infra::Types::String + attribute :type, Infra::Types::String + attribute :duration, Infra::Types::Integer + attribute :difficulty, Infra::Types::String + attribute :requirements, Infra::Types::Array.optional + attribute :reward, Infra::Types::Hash.optional + attribute :subquests, Infra::Types::Array.optional + end + + class QuestRequirementAdded < Infra::Event + attribute :quest_id, Infra::Types::UUID + attribute :requirement, Infra::Types::Hash + end + + class SubquestAdded < Infra::Event + attribute :quest_id, Infra::Types::UUID + attribute :subquest, Infra::Types::Hash + end + + class ActionAssociatedWithQuest < Infra::Event + attribute :quest_id, Infra::Types::UUID + attribute :action_id, Infra::Types::UUID end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb new file mode 100644 index 0000000..76f7634 --- /dev/null +++ b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb @@ -0,0 +1,28 @@ +# govquests/questing/lib/questing/on_quest_commands.rb +module Questing + class OnQuestCommands + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + @repository.with_aggregate(Quest, command.aggregate_id) do |quest| + case command + when CreateQuest + quest.create( + command.audience, + command.type, + command.duration, + command.difficulty, + command.requirements, + command.reward + ) + when AssociateActionWithQuest + quest.associate_action(command.action_id) + when UpdateQuestProgress + quest.update_progress(command.user_id, command.action_id) + end + end + end + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing/on_quest_created.rb b/apps/govquests-api/govquests/questing/lib/questing/on_quest_created.rb deleted file mode 100644 index 8f83cc9..0000000 --- a/apps/govquests-api/govquests/questing/lib/questing/on_quest_created.rb +++ /dev/null @@ -1,14 +0,0 @@ -# lib/questing/on_register_quest.rb -module Questing - class OnQuestCreated - def initialize(event_store) - @repository = Infra::AggregateRootRepository.new(event_store) - end - - def call(command) - @repository.with_aggregate(Quest, command.aggregate_id) do |quest| - quest.create(command.audience, command.type, command.duration, command.difficulty) - end - end - end -end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest.rb b/apps/govquests-api/govquests/questing/lib/questing/quest.rb index c33e2ce..965a511 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/quest.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/quest.rb @@ -1,24 +1,68 @@ +# app/models/quest.rb module Questing class Quest include AggregateRoot - AlreadyRegistered = Class.new(StandardError) - def initialize(id) @id = id + @audience = nil + @quest_type = nil + @duration = nil + @difficulty = nil + @requirements = [] + @reward = {} + @subquests = [] + @status = "created" + @progress = {} end - def register - raise AlreadyRegistered if @registered + def create(audience, quest_type, duration, difficulty, requirements = [], reward = {}, subquests = []) + raise "Quest already created" unless @status == "created" + + apply QuestCreated.new(data: { + quest_id: @id, + audience: audience, + quest_type: quest_type, + duration: duration, + difficulty: difficulty, + requirements: requirements, + reward: reward, + subquests: subquests + }) + end + + def associate_action(action_id) + apply ActionAssociatedWithQuest.new(data: {quest_id: @id, action_id: action_id}) + end - apply QuestCreated.new(data: {quest_id: @id}) + def update_progress(user_id, progress_measure) + apply QuestProgressUpdated.new(data: { + user_id: user_id, + quest_id: @id, + progress_measure: progress_measure + }) end private on QuestCreated do |event| - @registered = true - @status = :draft + @audience = event.data[:audience] + @quest_type = event.data[:quest_type] + @duration = event.data[:duration] + @difficulty = event.data[:difficulty] + @requirements = event.data[:requirements] + @reward = event.data[:reward] + @subquests = event.data[:subquests] + @status = "created" + end + + on ActionAssociatedWithQuest do |event| + @actions ||= [] + @actions << event.data[:action_id] + end + + on QuestProgressUpdated do |event| + @progress[event.data[:user_id]] = event.data[:progress_measure] end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb b/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb new file mode 100644 index 0000000..ffa156e --- /dev/null +++ b/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb @@ -0,0 +1,27 @@ +module Questing + class QuestAudience < Dry::Struct + values :AllUsers, :Delegates, :NewUsers + end + + class QuestType < Dry::Struct + values :Standard, :Epic, :Legendary + end + + class QuestStatus < Dry::Struct + values :Created, :Started, :Completed, :Expired, :Archived + end + + class QuestDifficulty < Dry::Struct + values :Easy, :Medium, :Hard, :Expert + end + + class QuestReward < Dry::Struct + attribute :reward_type, Infra::Types::String + attribute :reward_value, Infra::Types::Integer + end + + class QuestRequirement < Dry::Struct + attribute :type, Infra::Types::String + attribute :description, Infra::Types::String + end +end diff --git a/apps/govquests-api/govquests/questing/test/register_quest_test.rb b/apps/govquests-api/govquests/questing/test/register_quest_test.rb index 0c4bd2b..62c7126 100644 --- a/apps/govquests-api/govquests/questing/test/register_quest_test.rb +++ b/apps/govquests-api/govquests/questing/test/register_quest_test.rb @@ -1,12 +1,12 @@ require_relative "test_helper" module Questing - class RegisterQuestTest < Test + class CreateQuestTest < Test cover "Questing*" def setup @uid = SecureRandom.uuid - @data = { quest_id: @uid, } + @data = {quest_id: @uid} end def test_quest_should_get_registered @@ -26,7 +26,7 @@ def test_should_not_allow_for_double_registration private def register_quest(quest_id) - run_command(RegisterQuest.new(quest_id: quest_id)) + run_command(CreateQuest.new(quest_id: quest_id)) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/configuration.rb new file mode 100644 index 0000000..55db625 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/questing/configuration.rb @@ -0,0 +1,11 @@ +module Questing + class QuestReadModel < ApplicationRecord + self.table_name = "quests" + end + + class Configuration + def call(event_store, command_bus) + event_store.subscribe(CreateQuest, to: [Questing::QuestCreated]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/create_quest.rb b/apps/govquests-api/rails_app/app/read_models/questing/create_quest.rb new file mode 100644 index 0000000..f368907 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/questing/create_quest.rb @@ -0,0 +1,26 @@ +# app/read_models/quests/create_quest.rb +module Quests + class CreateQuest + def call(event) + quest_id = event.data.fetch(:quest_id) + audience = event.data.fetch(:audience) + quest_type = event.data.fetch(:quest_type) + duration = event.data.fetch(:duration) + difficulty = event.data.fetch(:difficulty) + requirements = event.data[:requirements] || [] + reward = event.data[:reward] || {} + subquests = event.data[:subquests] || [] + + Quest.find_or_create_by(quest_id: quest_id).update( + audience: audience, + quest_type: quest_type, + duration: duration, + difficulty: difficulty, + requirements: requirements, + reward: reward, + subquests: subquests, + status: "created" + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/update_quest_status.rb b/apps/govquests-api/rails_app/app/read_models/questing/update_quest_status.rb new file mode 100644 index 0000000..1098596 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/questing/update_quest_status.rb @@ -0,0 +1,11 @@ +module Quests + class UpdateQuestStatus + def call(event) + quest_id = event.data.fetch(:quest_id) + status = event.data.fetch(:status) + + quest = Quest.find_by(quest_id: quest_id) + quest.update_column(:status, status) + end + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb new file mode 100644 index 0000000..bb2cd4b --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb @@ -0,0 +1,16 @@ +class CreateQuests < ActiveRecord::Migration[8.0] + def change + create_table :quests do |t| + t.string :quest_id, null: false, index: {unique: true} + t.string :audience, null: false + t.string :quest_type, null: false + t.integer :duration, null: false + t.string :difficulty, null: false + t.jsonb :requirements, default: [] + t.jsonb :reward, default: {} + t.jsonb :subquests, default: [] + t.string :status, default: "created" + t.timestamps + end + end +end From 1838c4e0ee6d5e7a955d6bbf89028aec92388d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Wed, 2 Oct 2024 16:23:52 -0300 Subject: [PATCH 05/17] chore: refactor authentication domain --- .../authentication/lib/authentication.rb | 14 ++- .../lib/authentication/account.rb | 25 ---- .../lib/authentication/commands.rb | 33 ++++- .../lib/authentication/events.rb | 37 +++++- .../lib/authentication/on_register_account.rb | 13 -- .../lib/authentication/on_user_commands.rb | 22 ++++ .../authentication/lib/authentication/user.rb | 118 ++++++++++++++++++ .../lib/authentication/value_objects.rb | 12 ++ .../test/register_account_test.rb | 32 ----- .../authentication/test/register_user_test.rb | 35 ++++++ .../authentication/test/test_helper.rb | 2 +- .../test/update_quest_progress_test.rb | 37 ++++++ .../authentication/add_user_reward.rb | 11 ++ .../authentication/configuration.rb | 13 ++ .../read_models/authentication/create_user.rb | 15 +++ .../authentication/log_user_activity.rb | 16 +++ .../update_user_quest_progress.rb | 12 ++ .../client_authentication/configuration.rb | 11 -- .../client_authentication/create_account.rb | 11 -- .../db/migrate/20240930201737_create_users.rb | 33 +++++ .../test/client_authentication/create_test.rb | 13 +- 21 files changed, 402 insertions(+), 113 deletions(-) delete mode 100644 apps/govquests-api/govquests/authentication/lib/authentication/account.rb delete mode 100644 apps/govquests-api/govquests/authentication/lib/authentication/on_register_account.rb create mode 100644 apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb create mode 100644 apps/govquests-api/govquests/authentication/lib/authentication/user.rb create mode 100644 apps/govquests-api/govquests/authentication/lib/authentication/value_objects.rb delete mode 100644 apps/govquests-api/govquests/authentication/test/register_account_test.rb create mode 100644 apps/govquests-api/govquests/authentication/test/register_user_test.rb create mode 100644 apps/govquests-api/govquests/authentication/test/update_quest_progress_test.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/add_user_reward.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/configuration.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/create_user.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/log_user_activity.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/client_authentication/configuration.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/client_authentication/create_account.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb diff --git a/apps/govquests-api/govquests/authentication/lib/authentication.rb b/apps/govquests-api/govquests/authentication/lib/authentication.rb index 42dfeb5..9e0d10e 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication.rb @@ -1,13 +1,17 @@ require "infra" -require_relative "authentication/commands/register_account" -require_relative "authentication/events/account_registered" -require_relative "authentication/account_service" -require_relative "authentication/account" +require_relative "authentication/commands" +require_relative "authentication/events" +require_relative "authentication/on_user_commands" +require_relative "authentication/user" module Authentication class Configuration def call(event_store, command_bus) - command_bus.register(RegisterAccount, OnRegisterAccount.new(event_store)) + command_handler = OnUserCommands.new(event_store) + command_bus.register(RegisterUser, command_handler) + command_bus.register(UpdateQuestProgress, command_handler) + command_bus.register(ClaimReward, command_handler) + command_bus.register(LogUserActivity, command_handler) end end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/account.rb b/apps/govquests-api/govquests/authentication/lib/authentication/account.rb deleted file mode 100644 index cbba255..0000000 --- a/apps/govquests-api/govquests/authentication/lib/authentication/account.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Authentication - class Account - include AggregateRoot - - AlreadyRegistered = Class.new(StandardError) - - def initialize(id) - @id = id - end - - def register(address, chain_id) - raise AlreadyRegistered if @registered - - apply AccountRegistered.new(data: { account_id: @id, address: address, chain_id: chain_id }) - end - - private - - on AccountRegistered do |event| - @registered = true - @address = event.data[:address] - @chain_id = event.data[:chain_id] - end - end -end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb index 7fb55c2..a1cf161 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb @@ -1,9 +1,34 @@ module Authentication - class RegisterAccount < Infra::Command - attribute :account_id, Infra::Types::UUID + class RegisterUser < Infra::Command + attribute :user_id, Infra::Types::UUID attribute :address, Infra::Types::String - attribute :chain_id, Infra::Types::Value + attribute :chain_id, Infra::Types::Integer + attribute :email, Infra::Types::String.optional + attribute :user_type, Infra::Types::String - alias aggregate_id account_id + alias_method :aggregate_id, :user_id + end + + class UpdateQuestProgress < Infra::Command + attribute :user_id, Infra::Types::UUID + attribute :quest_id, Infra::Types::UUID + attribute :progress_measure, Infra::Types::Integer + + alias_method :aggregate_id, :user_id + end + + class ClaimReward < Infra::Command + attribute :user_id, Infra::Types::UUID + attribute :reward_id, Infra::Types::UUID + + alias_method :aggregate_id, :user_id + end + + class LogUserActivity < Infra::Command + attribute :user_id, Infra::Types::UUID + attribute :action_type, Infra::Types::String + attribute :action_timestamp, Infra::Types::Time + + alias_method :aggregate_id, :user_id end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/events.rb b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb index 58c6398..24c298f 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/events.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb @@ -1,7 +1,36 @@ module Authentication - class AccountRegistered < Infra::Event - attribute :account_id, Infra::Types::UUID - attribute :address, Infra::Types::String - attribute :chain_id, Infra::Types::Value + class UserRegistered < Infra::Event + attribute :user_id, Infra::Types::UUID + attribute :email, Infra::Types::String.optional + attribute :user_type, Infra::Types::String + end + + class WalletConnected < Infra::Event + attribute :user_id, Infra::Types::UUID + attribute :wallet_address, Infra::Types::String + attribute :chain_id, Infra::Types::Integer + end + + class UserLoggedIn < Infra::Event + attribute :user_id, Infra::Types::UUID + attribute :session_token, Infra::Types::String + attribute :timestamp, Infra::Types::Time + end + + class UserLoggedOut < Infra::Event + attribute :user_id, Infra::Types::UUID + attribute :session_token, Infra::Types::String + end + + class SessionExpired < Infra::Event + attribute :user_id, Infra::Types::UUID + attribute :session_token, Infra::Types::String + attribute :expired_at, Infra::Types::Time + end + + class QuestProgressUpdated < Infra::Event + attribute :user_id, Infra::Types::UUID + attribute :quest_id, Infra::Types::UUID + attribute :progress_measure, Infra::Types::Integer end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/on_register_account.rb b/apps/govquests-api/govquests/authentication/lib/authentication/on_register_account.rb deleted file mode 100644 index fe81537..0000000 --- a/apps/govquests-api/govquests/authentication/lib/authentication/on_register_account.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Authentication - class OnRegisterAccount - def initialize(event_store) - @repository = Infra::AggregateRootRepository.new(event_store) - end - - def call(command) - @repository.with_aggregate(Account, command.aggregate_id) do |account| - account.register(command.address, command.chain_id) - end - end - end -end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb b/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb new file mode 100644 index 0000000..afa2e0c --- /dev/null +++ b/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb @@ -0,0 +1,22 @@ +module Authentication + class OnUserCommands + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + @repository.with_aggregate(User, command.aggregate_id) do |user| + case command + when RegisterUser + user.register(command.email, command.user_type) + when UpdateQuestProgress + user.update_quest_progress(command.quest_id, command.progress_measure) + when ClaimReward + user.claim_reward(command.reward_id) + when LogUserActivity + user.log_activity(command.action_type, command.action_timestamp) + end + end + end + end +end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/user.rb b/apps/govquests-api/govquests/authentication/lib/authentication/user.rb new file mode 100644 index 0000000..f11b2b8 --- /dev/null +++ b/apps/govquests-api/govquests/authentication/lib/authentication/user.rb @@ -0,0 +1,118 @@ +# app/models/user.rb +module Authentication + class User + include AggregateRoot + + AlreadyRegistered = Class.new(StandardError) + + def initialize(id) + @id = id + @email = nil + @user_type = "non_delegate" + @address = nil + @chain_id = nil + @settings = {} + @wallets = [] + @sessions = [] + @quests_progress = {} + @activity_log = [] + @claimed_rewards = [] + end + + def register(email, user_type, address, chain_id) + raise AlreadyRegistered if @registered + + apply UserRegistered.new(data: { + user_id: @id, + email: email, + user_type: user_type, + address: address, + chain_id: chain_id + }) + end + + def connect_wallet(wallet_address, chain_id) + apply WalletConnected.new(data: { + user_id: @id, + wallet_address: wallet_address, + chain_id: chain_id + }) + end + + def log_in(session_token, timestamp) + apply UserLoggedIn.new(data: { + user_id: @id, + session_token: session_token, + timestamp: timestamp + }) + end + + def log_out(session_token) + apply UserLoggedOut.new(data: { + user_id: @id, + session_token: session_token + }) + end + + def expire_session(session_token, expired_at) + apply SessionExpired.new(data: { + user_id: @id, + session_token: session_token, + expired_at: expired_at + }) + end + + def update_quest_progress(quest_id, progress_measure) + apply QuestProgressUpdated.new(data: { + user_id: @id, + quest_id: quest_id, + progress_measure: progress_measure + }) + end + + def claim_reward(reward_id) + raise AlreadyClaimed if @claimed_rewards.include?(reward_id) + + apply RewardClaimed.new(data: { + user_id: @id, + reward_id: reward_id + }) + end + + private + + on UserRegistered do |event| + @registered = true + @email = event.data[:email] + @user_type = event.data[:user_type] + @address = event.data[:address] + @chain_id = event.data[:chain_id] + end + + on WalletConnected do |event| + @wallets << { + wallet_address: event.data[:wallet_address], + chain_id: event.data[:chain_id] + } + end + + on UserLoggedIn do |event| + @sessions << { + session_token: event.data[:session_token], + timestamp: event.data[:timestamp] + } + end + + on UserLoggedOut do |event| + @sessions.reject! { |s| s[:session_token] == event.data[:session_token] } + end + + on SessionExpired do |event| + @sessions.reject! { |s| s[:session_token] == event.data[:session_token] } + end + + on QuestProgressUpdated do |event| + @quests_progress[event.data[:quest_id]] = event.data[:progress_measure] + end + end +end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/value_objects.rb b/apps/govquests-api/govquests/authentication/lib/authentication/value_objects.rb new file mode 100644 index 0000000..7f4e2c6 --- /dev/null +++ b/apps/govquests-api/govquests/authentication/lib/authentication/value_objects.rb @@ -0,0 +1,12 @@ +module Authentication + class UserType < Dry::Struct + values :delegate, :non_delegate + end + + class EmailAddress < Dry::Struct + def initialize(value) + super + raise "Invalid email format" unless URI::MailTo::EMAIL_REGEXP.match?(value) + end + end +end diff --git a/apps/govquests-api/govquests/authentication/test/register_account_test.rb b/apps/govquests-api/govquests/authentication/test/register_account_test.rb deleted file mode 100644 index 5c380ff..0000000 --- a/apps/govquests-api/govquests/authentication/test/register_account_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require_relative "test_helper" - -module Authentication - class RegisterAccountTest < Test - cover "Authentication*" - - def setup - @uid = SecureRandom.uuid - @data = { account_id: @uid, address: "0x", chain_id: 1 } - end - - def test_account_should_get_registered - account_registered = AccountRegistered.new(data: @data) - assert_events("Authentication::Account$#{@uid}", account_registered) do - register_account(@uid) - end - end - - def test_should_not_allow_for_double_registration - assert_raises(Account::AlreadyRegistered) do - register_account(@uid) - register_account(@uid) - end - end - - private - - def register_account(account_id) - run_command(RegisterAccount.new(account_id: account_id, address: "0x", chain_id: 1)) - end - end -end diff --git a/apps/govquests-api/govquests/authentication/test/register_user_test.rb b/apps/govquests-api/govquests/authentication/test/register_user_test.rb new file mode 100644 index 0000000..b2a11a0 --- /dev/null +++ b/apps/govquests-api/govquests/authentication/test/register_user_test.rb @@ -0,0 +1,35 @@ +require_relative "test_helper" + +module Authentication + class RegisterUserTest < Test + cover "Authentication*" + + def setup + @user_id = SecureRandom.uuid + @email = "user@example.com" + @chain_id = 1 + @user_type = "delegate" + @data = {user_id: @user_id, email: @email, user_type: @user_type} + end + + def test_user_should_get_registered + user_registered = UserRegistered.new(data: @data) + assert_events("Authentication::User$#{@user_id}", user_registered) do + register_user(@user_id, @email, @chain_id, @user_type) + end + end + + def test_should_not_allow_double_registration + assert_raises(User::AlreadyRegistered) do + register_user(@user_id, @email, @chain_id, @user_type) + register_user(@user_id, @email, @chain_id, @user_type) + end + end + + private + + def register_user(user_id, email, chain_id, user_type) + run_command(RegisterUser.new(user_id: user_id, address: "0x123", chain_id: chain_id, email: email, user_type: user_type)) + end + end +end diff --git a/apps/govquests-api/govquests/authentication/test/test_helper.rb b/apps/govquests-api/govquests/authentication/test/test_helper.rb index bc727db..23e0a74 100644 --- a/apps/govquests-api/govquests/authentication/test/test_helper.rb +++ b/apps/govquests-api/govquests/authentication/test/test_helper.rb @@ -6,7 +6,7 @@ module Authentication class Test < Infra::InMemoryTest def before_setup - super() + super Configuration.new.call(event_store, command_bus) end diff --git a/apps/govquests-api/govquests/authentication/test/update_quest_progress_test.rb b/apps/govquests-api/govquests/authentication/test/update_quest_progress_test.rb new file mode 100644 index 0000000..9a87030 --- /dev/null +++ b/apps/govquests-api/govquests/authentication/test/update_quest_progress_test.rb @@ -0,0 +1,37 @@ +require_relative "test_helper" + +module Authentication + class UpdateQuestProgressTest < Test + cover "Authentication*" + + def setup + @user_id = SecureRandom.uuid + register_user(@user_id) + @quest_id = SecureRandom.uuid + end + + def test_user_can_update_quest_progress + progress_measure = 5 + quest_progress_updated = QuestProgressUpdated.new( + data: {user_id: @user_id, quest_id: @quest_id, progress_measure: progress_measure} + ) + assert_events("Authentication::User$#{@user_id}", quest_progress_updated) do + update_quest_progress(@user_id, @quest_id, progress_measure) + end + end + + private + + def register_user(user_id) + run_command(RegisterUser.new(user_id: user_id, address: "0x", chain_id: 1)) + end + + def update_quest_progress(user_id, quest_id, progress_measure) + run_command(UpdateQuestProgress.new( + user_id: user_id, + quest_id: quest_id, + progress_measure: progress_measure + )) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/add_user_reward.rb b/apps/govquests-api/rails_app/app/read_models/authentication/add_user_reward.rb new file mode 100644 index 0000000..e8801dc --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/authentication/add_user_reward.rb @@ -0,0 +1,11 @@ +module ClientAuthentication + class AddUserReward + def call(event) + user_id = event.data.fetch(:user_id) + reward_id = event.data.fetch(:reward_id) + + user = User.find_by(user_id: user_id) + user&.update_column(:last_reward_id, reward_id) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/configuration.rb b/apps/govquests-api/rails_app/app/read_models/authentication/configuration.rb new file mode 100644 index 0000000..79ae2a0 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/authentication/configuration.rb @@ -0,0 +1,13 @@ +module Authentication + class UserReadModel < ApplicationRecord + self.table_name = "users" + end + + class Configuration + def call(event_store, command_bus) + event_store.subscribe(CreateUser, to: [Authentication::UserRegistered]) + event_store.subscribe(UpdateUserQuestProgress, to: [Authentication::QuestProgressUpdated]) + event_store.subscribe(AddUserReward, to: [Authentication::RewardClaimed]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/create_user.rb b/apps/govquests-api/rails_app/app/read_models/authentication/create_user.rb new file mode 100644 index 0000000..b6c3789 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/authentication/create_user.rb @@ -0,0 +1,15 @@ +# app/read_models/client_authentication/create_user.rb +module ClientAuthentication + class CreateUser + def call(event) + user_id = event.data.fetch(:user_id) + email = event.data[:email] + user_type = event.data[:user_type] + + User.find_or_create_by(user_id: user_id).update( + email: email, + user_type: user_type + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/log_user_activity.rb b/apps/govquests-api/rails_app/app/read_models/authentication/log_user_activity.rb new file mode 100644 index 0000000..3c1f77c --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/authentication/log_user_activity.rb @@ -0,0 +1,16 @@ +module ClientAuthentication + class LogUserActivity + def call(event) + user_id = event.data.fetch(:user_id) + action_type = event.data.fetch(:action_type) + action_timestamp = event.data.fetch(:action_timestamp) + + user = User.find_by(user_id: user_id) + if user + user.activities ||= [] + user.activities << {action_type: action_type, timestamp: action_timestamp} + user.save + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb b/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb new file mode 100644 index 0000000..0a71d90 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb @@ -0,0 +1,12 @@ +module ClientAuthentication + class UpdateUserQuestProgress + def call(event) + user_id = event.data.fetch(:user_id) + quest_id = event.data.fetch(:quest_id) + progress_measure = event.data.fetch(:progress_measure) + + user = User.find_by(user_id: user_id) + user&.update_columns(last_quest_id: quest_id, progress_measure: progress_measure) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/client_authentication/configuration.rb b/apps/govquests-api/rails_app/app/read_models/client_authentication/configuration.rb deleted file mode 100644 index 8bd3100..0000000 --- a/apps/govquests-api/rails_app/app/read_models/client_authentication/configuration.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ClientAuthentication - class Account < ApplicationRecord - self.table_name = "accounts" - end - - class Configuration - def call(event_store) - event_store.subscribe(CreateAccount, to: [ Authentication::AccountRegistered ]) - end - end -end diff --git a/apps/govquests-api/rails_app/app/read_models/client_authentication/create_account.rb b/apps/govquests-api/rails_app/app/read_models/client_authentication/create_account.rb deleted file mode 100644 index 0721d00..0000000 --- a/apps/govquests-api/rails_app/app/read_models/client_authentication/create_account.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ClientAuthentication - class CreateAccount - def call(event) - account_id = event.data.fetch(:account_id) - Account.find_or_create_by(account_id: account_id).update( - address: event.data.fetch(:address), - chain_id: event.data.fetch(:chain_id) - ) - end - end -end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb b/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb new file mode 100644 index 0000000..a8588ef --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb @@ -0,0 +1,33 @@ +class CreateUsers < ActiveRecord::Migration[8.0] + def change + create_table :users do |t| + t.string :user_id, null: false, index: {unique: true} + t.string :email, null: false, index: {unique: true} + t.string :user_type, null: false, default: "non_delegate" + t.jsonb :settings, default: {} + t.jsonb :wallets, default: [] + t.jsonb :sessions, default: [] + t.jsonb :quests_progress, default: {} + t.jsonb :activity_log, default: [] + t.timestamps + end + + create_table :user_sessions do |t| + t.string :user_id, null: false + t.string :session_token, null: false + t.datetime :logged_in_at, null: false + t.datetime :logged_out_at + t.timestamps + end + + create_table :user_rewards do |t| + t.string :user_id, null: false + t.string :reward_id, null: false + t.string :status, default: "pending" + t.timestamps + end + + add_index :user_sessions, :user_id + add_index :user_rewards, [:user_id, :reward_id], unique: true + end +end diff --git a/apps/govquests-api/rails_app/test/client_authentication/create_test.rb b/apps/govquests-api/rails_app/test/client_authentication/create_test.rb index d1235d6..4f3b2ac 100644 --- a/apps/govquests-api/rails_app/test/client_authentication/create_test.rb +++ b/apps/govquests-api/rails_app/test/client_authentication/create_test.rb @@ -6,25 +6,24 @@ class CreateTest < InMemoryTestCase def setup super - ClientAuthentication::Account.destroy_all + ClientAuthentication::User.destroy_all end def test_set_create - account_id = SecureRandom.uuid + user_id = SecureRandom.uuid address = "0x" chain_id = 1 - run_command( - Authentication::RegisterAccount.new( - account_id: account_id, + Authentication::RegisterUser.new( + user_id: user_id, address: address, chain_id: chain_id ) ) - account = ClientAuthentication::Account.find_by(account_id: account_id, address: address, chain_id: chain_id) - assert account.present? + user = ClientAuthentication::User.find_by(user_id: user_id, address: address, chain_id: chain_id) + assert user.present? end private From ad3f5e933f7f6537db8dbc4779719928ae2c2724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Wed, 2 Oct 2024 16:53:17 -0300 Subject: [PATCH 06/17] chore: refactor domains inner workings --- .../authentication/lib/authentication.rb | 1 - .../lib/authentication/commands.rb | 7 -- .../lib/authentication/events.rb | 6 -- .../authentication/lib/authentication/user.rb | 21 ----- .../test/update_quest_progress_test.rb | 37 -------- .../govquests/questing/lib/questing.rb | 8 +- .../govquests/questing/lib/questing/quest.rb | 12 --- .../questing/test/register_quest_test.rb | 32 ------- apps/govquests-api/rails_app/.dockerignore | 14 +-- apps/govquests-api/rails_app/Dockerfile | 44 ++++----- apps/govquests-api/rails_app/Gemfile | 6 +- apps/govquests-api/rails_app/Gemfile.lock | 67 +++++--------- .../authentication/configuration.rb | 13 --- ...d_user_reward.rb => on_add_user_reward.rb} | 4 +- .../{create_user.rb => on_create_user.rb} | 4 +- ...er_activity.rb => on_log_user_activity.rb} | 6 +- .../read_model_configuration.rb | 11 +++ .../update_user_quest_progress.rb | 2 +- .../app/read_models/questing/configuration.rb | 11 --- .../{create_quest.rb => on_quest_created.rb} | 5 +- ...t_status.rb => on_quest_status_updated.rb} | 4 +- .../questing/read_model_configuration.rb | 11 +++ .../app/read_models/quests/configuration.rb | 11 --- .../app/read_models/quests/create_quest.rb | 8 -- .../app/views/layouts/application.html.erb | 15 ++++ .../rails_app/bin/docker-entrypoint | 8 +- apps/govquests-api/rails_app/bin/setup | 10 +-- .../rails_app/config/database.yml | 89 +++++++++++++++---- .../config/initializers/rails_event_store.rb | 5 +- apps/govquests-api/rails_app/config/routes.rb | 1 - .../migrate/20240927211319_create_accounts.rb | 12 --- .../migrate/20240930172742_create_quests.rb | 9 -- .../migrate/20240930201726_create_quests.rb | 2 +- .../db/migrate/20240930201737_create_users.rb | 6 +- ...ration.rb => read_models_configuration.rb} | 8 +- .../govquests-api/rails_app/public/robots.txt | 1 - 36 files changed, 185 insertions(+), 326 deletions(-) delete mode 100644 apps/govquests-api/govquests/authentication/test/update_quest_progress_test.rb delete mode 100644 apps/govquests-api/govquests/questing/test/register_quest_test.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/configuration.rb rename apps/govquests-api/rails_app/app/read_models/authentication/{add_user_reward.rb => on_add_user_reward.rb} (81%) rename apps/govquests-api/rails_app/app/read_models/authentication/{create_user.rb => on_create_user.rb} (87%) rename apps/govquests-api/rails_app/app/read_models/authentication/{log_user_activity.rb => on_log_user_activity.rb} (69%) create mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/questing/configuration.rb rename apps/govquests-api/rails_app/app/read_models/questing/{create_quest.rb => on_quest_created.rb} (90%) rename apps/govquests-api/rails_app/app/read_models/questing/{update_quest_status.rb => on_quest_status_updated.rb} (82%) create mode 100644 apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/quests/configuration.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/quests/create_quest.rb create mode 100644 apps/govquests-api/rails_app/app/views/layouts/application.html.erb delete mode 100644 apps/govquests-api/rails_app/db/migrate/20240927211319_create_accounts.rb delete mode 100644 apps/govquests-api/rails_app/db/migrate/20240930172742_create_quests.rb rename apps/govquests-api/rails_app/lib/{configuration.rb => read_models_configuration.rb} (82%) delete mode 100644 apps/govquests-api/rails_app/public/robots.txt diff --git a/apps/govquests-api/govquests/authentication/lib/authentication.rb b/apps/govquests-api/govquests/authentication/lib/authentication.rb index 9e0d10e..e257baa 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication.rb @@ -10,7 +10,6 @@ def call(event_store, command_bus) command_handler = OnUserCommands.new(event_store) command_bus.register(RegisterUser, command_handler) command_bus.register(UpdateQuestProgress, command_handler) - command_bus.register(ClaimReward, command_handler) command_bus.register(LogUserActivity, command_handler) end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb index a1cf161..7f89ee0 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb @@ -17,13 +17,6 @@ class UpdateQuestProgress < Infra::Command alias_method :aggregate_id, :user_id end - class ClaimReward < Infra::Command - attribute :user_id, Infra::Types::UUID - attribute :reward_id, Infra::Types::UUID - - alias_method :aggregate_id, :user_id - end - class LogUserActivity < Infra::Command attribute :user_id, Infra::Types::UUID attribute :action_type, Infra::Types::String diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/events.rb b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb index 24c298f..ea7d0bf 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/events.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb @@ -27,10 +27,4 @@ class SessionExpired < Infra::Event attribute :session_token, Infra::Types::String attribute :expired_at, Infra::Types::Time end - - class QuestProgressUpdated < Infra::Event - attribute :user_id, Infra::Types::UUID - attribute :quest_id, Infra::Types::UUID - attribute :progress_measure, Infra::Types::Integer - end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/user.rb b/apps/govquests-api/govquests/authentication/lib/authentication/user.rb index f11b2b8..bc827f3 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/user.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/user.rb @@ -62,23 +62,6 @@ def expire_session(session_token, expired_at) }) end - def update_quest_progress(quest_id, progress_measure) - apply QuestProgressUpdated.new(data: { - user_id: @id, - quest_id: quest_id, - progress_measure: progress_measure - }) - end - - def claim_reward(reward_id) - raise AlreadyClaimed if @claimed_rewards.include?(reward_id) - - apply RewardClaimed.new(data: { - user_id: @id, - reward_id: reward_id - }) - end - private on UserRegistered do |event| @@ -110,9 +93,5 @@ def claim_reward(reward_id) on SessionExpired do |event| @sessions.reject! { |s| s[:session_token] == event.data[:session_token] } end - - on QuestProgressUpdated do |event| - @quests_progress[event.data[:quest_id]] = event.data[:progress_measure] - end end end diff --git a/apps/govquests-api/govquests/authentication/test/update_quest_progress_test.rb b/apps/govquests-api/govquests/authentication/test/update_quest_progress_test.rb deleted file mode 100644 index 9a87030..0000000 --- a/apps/govquests-api/govquests/authentication/test/update_quest_progress_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -require_relative "test_helper" - -module Authentication - class UpdateQuestProgressTest < Test - cover "Authentication*" - - def setup - @user_id = SecureRandom.uuid - register_user(@user_id) - @quest_id = SecureRandom.uuid - end - - def test_user_can_update_quest_progress - progress_measure = 5 - quest_progress_updated = QuestProgressUpdated.new( - data: {user_id: @user_id, quest_id: @quest_id, progress_measure: progress_measure} - ) - assert_events("Authentication::User$#{@user_id}", quest_progress_updated) do - update_quest_progress(@user_id, @quest_id, progress_measure) - end - end - - private - - def register_user(user_id) - run_command(RegisterUser.new(user_id: user_id, address: "0x", chain_id: 1)) - end - - def update_quest_progress(user_id, quest_id, progress_measure) - run_command(UpdateQuestProgress.new( - user_id: user_id, - quest_id: quest_id, - progress_measure: progress_measure - )) - end - end -end diff --git a/apps/govquests-api/govquests/questing/lib/questing.rb b/apps/govquests-api/govquests/questing/lib/questing.rb index 91832cb..5b36034 100644 --- a/apps/govquests-api/govquests/questing/lib/questing.rb +++ b/apps/govquests-api/govquests/questing/lib/questing.rb @@ -4,13 +4,13 @@ require_relative "questing/on_quest_commands" require_relative "questing/quest" -# app/read_models/quests/configuration.rb module Questing class Configuration def call(event_store, command_bus) - event_store.subscribe(CreateQuest, to: [Questing::QuestCreated]) - event_store.subscribe(UpdateQuestStatus, to: [Questing::QuestProgressUpdated]) - # Add additional subscriptions as needed + command_handler = OnQuestCommands.new(event_store) + command_bus.register(CreateQuest, command_handler) + command_bus.register(AssociateActionWithQuest, command_handler) + command_bus.register(UpdateQuestProgress, command_handler) end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest.rb b/apps/govquests-api/govquests/questing/lib/questing/quest.rb index 965a511..514e00c 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/quest.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/quest.rb @@ -35,14 +35,6 @@ def associate_action(action_id) apply ActionAssociatedWithQuest.new(data: {quest_id: @id, action_id: action_id}) end - def update_progress(user_id, progress_measure) - apply QuestProgressUpdated.new(data: { - user_id: user_id, - quest_id: @id, - progress_measure: progress_measure - }) - end - private on QuestCreated do |event| @@ -60,9 +52,5 @@ def update_progress(user_id, progress_measure) @actions ||= [] @actions << event.data[:action_id] end - - on QuestProgressUpdated do |event| - @progress[event.data[:user_id]] = event.data[:progress_measure] - end end end diff --git a/apps/govquests-api/govquests/questing/test/register_quest_test.rb b/apps/govquests-api/govquests/questing/test/register_quest_test.rb deleted file mode 100644 index 62c7126..0000000 --- a/apps/govquests-api/govquests/questing/test/register_quest_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require_relative "test_helper" - -module Questing - class CreateQuestTest < Test - cover "Questing*" - - def setup - @uid = SecureRandom.uuid - @data = {quest_id: @uid} - end - - def test_quest_should_get_registered - quest_registered = QuestRegistered.new(data: @data) - assert_events("Questing::Quest$#{@uid}", quest_registered) do - register_quest(@uid) - end - end - - def test_should_not_allow_for_double_registration - assert_raises(Quest::AlreadyRegistered) do - register_quest(@uid) - register_quest(@uid) - end - end - - private - - def register_quest(quest_id) - run_command(CreateQuest.new(quest_id: quest_id)) - end - end -end diff --git a/apps/govquests-api/rails_app/.dockerignore b/apps/govquests-api/rails_app/.dockerignore index 98a89a3..bb56daf 100644 --- a/apps/govquests-api/rails_app/.dockerignore +++ b/apps/govquests-api/rails_app/.dockerignore @@ -2,13 +2,13 @@ # Ignore git directory. /.git/ -/.gitignore # Ignore bundler config. /.bundle -# Ignore all environment files. +# Ignore all environment files (except templates). /.env* +!/.env*.erb # Ignore all default key files. /config/master.key @@ -29,13 +29,3 @@ !/storage/.keep /tmp/storage/* !/tmp/storage/.keep - -# Ignore CI service files. -/.github - -# Ignore development files -/.devcontainer - -# Ignore Docker-related files -/.dockerignore -/Dockerfile* diff --git a/apps/govquests-api/rails_app/Dockerfile b/apps/govquests-api/rails_app/Dockerfile index 832a91c..9c28fe4 100644 --- a/apps/govquests-api/rails_app/Dockerfile +++ b/apps/govquests-api/rails_app/Dockerfile @@ -1,37 +1,25 @@ -# syntax=docker/dockerfile:1 -# check=error=true +# syntax = docker/dockerfile:1 -# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: -# docker build -t govquests_api . -# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name govquests_api govquests_api - -# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html - -# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile ARG RUBY_VERSION=3.3.0 -FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base # Rails app lives here WORKDIR /rails -# Install base packages -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives - # Set production environment ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" + # Throw-away build stage to reduce size of final image -FROM base AS build +FROM base as build # Install packages needed to build gems RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y build-essential git pkg-config && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives + apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config # Install application gems COPY Gemfile Gemfile.lock ./ @@ -46,24 +34,26 @@ COPY . . RUN bundle exec bootsnap precompile app/ lib/ - - # Final stage for app image FROM base +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libvips postgresql-client && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + # Copy built artifacts: gems, application -COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /usr/local/bundle /usr/local/bundle COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security -RUN groupadd --system --gid 1000 rails && \ - useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ +RUN useradd rails --create-home --shell /bin/bash && \ chown -R rails:rails db log storage tmp -USER 1000:1000 +USER rails:rails # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] -# Start server via Thruster by default, this can be overwritten at runtime -EXPOSE 80 -CMD ["./bin/thrust", "./bin/rails", "server"] +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["./bin/rails", "server"] diff --git a/apps/govquests-api/rails_app/Gemfile b/apps/govquests-api/rails_app/Gemfile index c746f36..31841b7 100644 --- a/apps/govquests-api/rails_app/Gemfile +++ b/apps/govquests-api/rails_app/Gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" # Use main development branch of Rails gem "rails", github: "rails/rails", branch: "main" # Use sqlite3 as the database for Active Record -gem "sqlite3", ">= 2.1" +gem "pg", "~> 1.1" # Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" # Build JSON APIs with ease [https://github.com/rails/jbuilder] @@ -13,7 +13,7 @@ gem "puma", ">= 5.0" # gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem "tzinfo-data", platforms: %i[ windows jruby ] +gem "tzinfo-data", platforms: %i[windows jruby] # Use the database-backed adapters for Rails.cache and Active Job gem "solid_cache" @@ -36,7 +36,7 @@ gem "thruster", require: false group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + gem "debug", platforms: %i[mri windows], require: "debug/prelude" # Static analysis for security vulnerabilities [https://brakemanscanner.org/] gem "brakeman", require: false diff --git a/apps/govquests-api/rails_app/Gemfile.lock b/apps/govquests-api/rails_app/Gemfile.lock index bade7af..cebafc8 100644 --- a/apps/govquests-api/rails_app/Gemfile.lock +++ b/apps/govquests-api/rails_app/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/rails/rails.git - revision: 15ddce90583bdf169ae69449b42db10be9f714c9 + revision: fa152fba87f3a11790783207e6e84e58d345c012 branch: main specs: actioncable (8.0.0.beta1) @@ -183,7 +183,7 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.7.2) - kamal (2.0.0) + kamal (2.1.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) @@ -231,17 +231,13 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.0) net-protocol - net-ssh (7.2.3) + net-ssh (7.3.0) netrc (0.11.0) nio4r (2.7.3) nokogiri (1.16.7-aarch64-linux) racc (~> 1.4) - nokogiri (1.16.7-arm-linux) - racc (~> 1.4) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86-linux) - racc (~> 1.4) nokogiri (1.16.7-x86_64-darwin) racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) @@ -251,7 +247,8 @@ GEM parser (3.3.5.0) ast (~> 2.4.1) racc - prism (0.30.0) + pg (1.5.8) + prism (1.1.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -302,8 +299,8 @@ GEM ruby_event_store-active_record (= 2.15.0) rainbow (3.1.1) rake (13.2.1) - rbi (0.1.13) - prism (>= 0.18.0, < 1.0.0) + rbi (0.2.0) + prism (~> 1.0) sorbet-runtime (>= 0.5.9204) rdoc (6.7.0) psych (>= 4.0.0) @@ -365,28 +362,20 @@ GEM fugit (~> 1.11.0) railties (>= 7.1) thor (~> 1.3.1) - sorbet (0.5.11463) - sorbet-static (= 0.5.11463) - sorbet-runtime (0.5.11463) - sorbet-static (0.5.11463-universal-darwin) - sorbet-static-and-runtime (0.5.11463) - sorbet (= 0.5.11463) - sorbet-runtime (= 0.5.11463) - spoom (1.3.2) + sorbet (0.5.11589) + sorbet-static (= 0.5.11589) + sorbet-runtime (0.5.11589) + sorbet-static (0.5.11589-aarch64-linux) + sorbet-static (0.5.11589-universal-darwin) + sorbet-static (0.5.11589-x86_64-linux) + sorbet-static-and-runtime (0.5.11589) + sorbet (= 0.5.11589) + sorbet-runtime (= 0.5.11589) + spoom (1.5.0) erubi (>= 1.10.0) - prism (>= 0.19.0) + prism (>= 0.28.0) sorbet-static-and-runtime (>= 0.5.10187) thor (>= 0.19.2) - sqlite3 (2.1.0-aarch64-linux-gnu) - sqlite3 (2.1.0-aarch64-linux-musl) - sqlite3 (2.1.0-arm-linux-gnu) - sqlite3 (2.1.0-arm-linux-musl) - sqlite3 (2.1.0-arm64-darwin) - sqlite3 (2.1.0-x86-linux-gnu) - sqlite3 (2.1.0-x86-linux-musl) - sqlite3 (2.1.0-x86_64-darwin) - sqlite3 (2.1.0-x86_64-linux-gnu) - sqlite3 (2.1.0-x86_64-linux-musl) sshkit (1.23.1) base64 net-scp (>= 1.1.2) @@ -394,17 +383,16 @@ GEM net-ssh (>= 2.8.0) ostruct stringio (3.1.1) - tapioca (0.15.0) + tapioca (0.16.2) bundler (>= 2.2.25) netrc (>= 0.11.0) parallel (>= 1.21.0) - rbi (>= 0.1.4, < 0.2) + rbi (~> 0.2) sorbet-static-and-runtime (>= 0.5.11087) spoom (>= 1.2.0) thor (>= 1.2.0) yard-sorbet thor (1.3.2) - thruster (0.1.8) thruster (0.1.8-aarch64-linux) thruster (0.1.8-arm64-darwin) thruster (0.1.8-x86_64-darwin) @@ -422,7 +410,7 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - yard (0.9.36) + yard (0.9.37) yard-sorbet (0.9.0) sorbet-runtime yard @@ -430,19 +418,10 @@ GEM PLATFORMS aarch64-linux - aarch64-linux-gnu - aarch64-linux-musl - arm-linux - arm-linux-gnu - arm-linux-musl arm64-darwin - x86-linux - x86-linux-gnu - x86-linux-musl + universal-darwin x86_64-darwin x86_64-linux - x86_64-linux-gnu - x86_64-linux-musl DEPENDENCIES bootsnap @@ -453,6 +432,7 @@ DEPENDENCIES minitest (= 5.25.0)! mutant-license! mutant-minitest (= 0.12.4)! + pg (~> 1.1) pry-byebug pry-meta pry-rails (~> 0.3.9) @@ -467,7 +447,6 @@ DEPENDENCIES solid_cache solid_queue sorbet - sqlite3 (>= 2.1) tapioca thruster tzinfo-data diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/configuration.rb b/apps/govquests-api/rails_app/app/read_models/authentication/configuration.rb deleted file mode 100644 index 79ae2a0..0000000 --- a/apps/govquests-api/rails_app/app/read_models/authentication/configuration.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Authentication - class UserReadModel < ApplicationRecord - self.table_name = "users" - end - - class Configuration - def call(event_store, command_bus) - event_store.subscribe(CreateUser, to: [Authentication::UserRegistered]) - event_store.subscribe(UpdateUserQuestProgress, to: [Authentication::QuestProgressUpdated]) - event_store.subscribe(AddUserReward, to: [Authentication::RewardClaimed]) - end - end -end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/add_user_reward.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_add_user_reward.rb similarity index 81% rename from apps/govquests-api/rails_app/app/read_models/authentication/add_user_reward.rb rename to apps/govquests-api/rails_app/app/read_models/authentication/on_add_user_reward.rb index e8801dc..9764649 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/add_user_reward.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_add_user_reward.rb @@ -1,5 +1,5 @@ -module ClientAuthentication - class AddUserReward +module Authentication + class OnAddUserReward def call(event) user_id = event.data.fetch(:user_id) reward_id = event.data.fetch(:reward_id) diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/create_user.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb similarity index 87% rename from apps/govquests-api/rails_app/app/read_models/authentication/create_user.rb rename to apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb index b6c3789..89efd58 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/create_user.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb @@ -1,6 +1,6 @@ # app/read_models/client_authentication/create_user.rb -module ClientAuthentication - class CreateUser +module Authentication + class OnCreateUser def call(event) user_id = event.data.fetch(:user_id) email = event.data[:email] diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/log_user_activity.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_log_user_activity.rb similarity index 69% rename from apps/govquests-api/rails_app/app/read_models/authentication/log_user_activity.rb rename to apps/govquests-api/rails_app/app/read_models/authentication/on_log_user_activity.rb index 3c1f77c..4171ec3 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/log_user_activity.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_log_user_activity.rb @@ -1,5 +1,5 @@ -module ClientAuthentication - class LogUserActivity +module Authentication + class OnLogUserActivity def call(event) user_id = event.data.fetch(:user_id) action_type = event.data.fetch(:action_type) @@ -8,7 +8,7 @@ def call(event) user = User.find_by(user_id: user_id) if user user.activities ||= [] - user.activities << {action_type: action_type, timestamp: action_timestamp} + user.activities << { action_type: action_type, timestamp: action_timestamp } user.save end end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb new file mode 100644 index 0000000..d14004d --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb @@ -0,0 +1,11 @@ +module Authentication + class UserReadModel < ApplicationRecord + self.table_name = "users" + end + + class ReadModelConfiguration + def call(event_store) + event_store.subscribe(OnCreateUser, to: [ Authentication::UserRegistered ]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb b/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb index 0a71d90..c62e02b 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb @@ -1,4 +1,4 @@ -module ClientAuthentication +module Authentication class UpdateUserQuestProgress def call(event) user_id = event.data.fetch(:user_id) diff --git a/apps/govquests-api/rails_app/app/read_models/questing/configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/configuration.rb deleted file mode 100644 index 55db625..0000000 --- a/apps/govquests-api/rails_app/app/read_models/questing/configuration.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Questing - class QuestReadModel < ApplicationRecord - self.table_name = "quests" - end - - class Configuration - def call(event_store, command_bus) - event_store.subscribe(CreateQuest, to: [Questing::QuestCreated]) - end - end -end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/create_quest.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb similarity index 90% rename from apps/govquests-api/rails_app/app/read_models/questing/create_quest.rb rename to apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb index f368907..aa9b559 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/create_quest.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb @@ -1,6 +1,5 @@ -# app/read_models/quests/create_quest.rb -module Quests - class CreateQuest +module Questing + class OnQuestCreated def call(event) quest_id = event.data.fetch(:quest_id) audience = event.data.fetch(:audience) diff --git a/apps/govquests-api/rails_app/app/read_models/questing/update_quest_status.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_status_updated.rb similarity index 82% rename from apps/govquests-api/rails_app/app/read_models/questing/update_quest_status.rb rename to apps/govquests-api/rails_app/app/read_models/questing/on_quest_status_updated.rb index 1098596..5a8c334 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/update_quest_status.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_status_updated.rb @@ -1,5 +1,5 @@ -module Quests - class UpdateQuestStatus +module Questing + class OnQuestStatusUpdated def call(event) quest_id = event.data.fetch(:quest_id) status = event.data.fetch(:status) diff --git a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb new file mode 100644 index 0000000..90310a3 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb @@ -0,0 +1,11 @@ +module Questing + class QuestReadModel < ApplicationRecord + self.table_name = "quests" + end + + class ReadModelConfiguration + def call(event_store) + event_store.subscribe(OnQuestCreated, to: [ Questing::QuestCreated ]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/quests/configuration.rb b/apps/govquests-api/rails_app/app/read_models/quests/configuration.rb deleted file mode 100644 index d44a769..0000000 --- a/apps/govquests-api/rails_app/app/read_models/quests/configuration.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Quests - class Quest < ApplicationRecord - self.table_name = "quests" - end - - class Configuration - def call(event_store) - event_store.subscribe(CreateQuest, to: [ Questing::QuestRegistered ]) - end - end -end diff --git a/apps/govquests-api/rails_app/app/read_models/quests/create_quest.rb b/apps/govquests-api/rails_app/app/read_models/quests/create_quest.rb deleted file mode 100644 index c587562..0000000 --- a/apps/govquests-api/rails_app/app/read_models/quests/create_quest.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Quests - class CreateQuest - def call(event) - quest_id = event.data.fetch(:quest_id) - Quest.find_or_create_by(quest_id: quest_id) - end - end -end diff --git a/apps/govquests-api/rails_app/app/views/layouts/application.html.erb b/apps/govquests-api/rails_app/app/views/layouts/application.html.erb new file mode 100644 index 0000000..d10b8b4 --- /dev/null +++ b/apps/govquests-api/rails_app/app/views/layouts/application.html.erb @@ -0,0 +1,15 @@ + + + + RailsApp + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "application" %> + + + + <%= yield %> + + diff --git a/apps/govquests-api/rails_app/bin/docker-entrypoint b/apps/govquests-api/rails_app/bin/docker-entrypoint index 57567d6..67ef493 100755 --- a/apps/govquests-api/rails_app/bin/docker-entrypoint +++ b/apps/govquests-api/rails_app/bin/docker-entrypoint @@ -1,13 +1,7 @@ #!/bin/bash -e -# Enable jemalloc for reduced memory usage and latency. -if [ -z "${LD_PRELOAD+x}" ]; then - LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) - export LD_PRELOAD -fi - # If running the rails server then create or migrate existing database -if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then +if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then ./bin/rails db:prepare fi diff --git a/apps/govquests-api/rails_app/bin/setup b/apps/govquests-api/rails_app/bin/setup index 4c35bc4..3cd5a9d 100755 --- a/apps/govquests-api/rails_app/bin/setup +++ b/apps/govquests-api/rails_app/bin/setup @@ -1,8 +1,8 @@ #!/usr/bin/env ruby require "fileutils" +# path to your application root. APP_ROOT = File.expand_path("..", __dir__) -APP_NAME = "govquests-api" def system!(*args) system(*args, exception: true) @@ -14,6 +14,7 @@ FileUtils.chdir APP_ROOT do # Add necessary setup steps to this file. puts "== Installing dependencies ==" + system! "gem install bundler --conservative" system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" @@ -27,9 +28,6 @@ FileUtils.chdir APP_ROOT do puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" - unless ARGV.include?("--skip-server") - puts "\n== Starting development server ==" - STDOUT.flush # flush the output before exec(2) so that it displays - exec "bin/dev" - end + puts "\n== Restarting application server ==" + system! "bin/rails restart" end diff --git a/apps/govquests-api/rails_app/config/database.yml b/apps/govquests-api/rails_app/config/database.yml index 8172692..8e3c159 100644 --- a/apps/govquests-api/rails_app/config/database.yml +++ b/apps/govquests-api/rails_app/config/database.yml @@ -1,36 +1,93 @@ -# SQLite. Versions 3.8.0 and up are supported. -# gem install sqlite3 +# PostgreSQL. Versions 9.3 and up are supported. # -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem "sqlite3" +# Install the pg driver: +# gem install pg +# On macOS with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem "pg" # default: &default - adapter: sqlite3 + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # https://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> - timeout: 5000 development: <<: *default - database: storage/development.sqlite3 + database: govquests_api_development + + # The specified database role being used to connect to PostgreSQL. + # To create additional roles in PostgreSQL see `$ createuser --help`. + # When left blank, PostgreSQL will use the default role. This is + # the same name as the operating system user running Rails. + #username: govquests_api + + # The password associated with the PostgreSQL role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default - database: storage/test.sqlite3 + database: govquests_api_test -# Store production database in the storage/ directory, which by default -# is mounted as a persistent Docker volume in config/deploy.yml. +# As with config/credentials.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password or a full connection URL as an environment +# variable when you boot the app. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%= ENV["MY_APP_DATABASE_URL"] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# production: - primary: + primary: &primary_production <<: *default - database: storage/production.sqlite3 + database: govquests_api_production + username: govquests_api + password: <%= ENV["GOVQUESTS_API_DATABASE_PASSWORD"] %> cache: - <<: *default - database: storage/production_cache.sqlite3 + <<: *primary_production + database: govquests_api_production_cache migrations_paths: db/cache_migrate queue: - <<: *default - database: storage/production_queue.sqlite3 + <<: *primary_production + database: govquests_api_production_queue migrations_paths: db/queue_migrate diff --git a/apps/govquests-api/rails_app/config/initializers/rails_event_store.rb b/apps/govquests-api/rails_app/config/initializers/rails_event_store.rb index bf0fa4c..6c94f76 100644 --- a/apps/govquests-api/rails_app/config/initializers/rails_event_store.rb +++ b/apps/govquests-api/rails_app/config/initializers/rails_event_store.rb @@ -2,8 +2,7 @@ require "aggregate_root" require "arkency/command_bus" -require_relative "../../lib/configuration" - +require_relative "../../lib/read_models_configuration" Rails.configuration.to_prepare do Rails.configuration.event_store = RailsEventStore::Client.new @@ -13,5 +12,5 @@ config.default_event_store = Rails.configuration.event_store end - Configuration.new.call(Rails.configuration.event_store, Rails.configuration.command_bus) + ReadModelsConfiguration.new.call(Rails.configuration.event_store, Rails.configuration.command_bus) end diff --git a/apps/govquests-api/rails_app/config/routes.rb b/apps/govquests-api/rails_app/config/routes.rb index e58bbac..a125ef0 100644 --- a/apps/govquests-api/rails_app/config/routes.rb +++ b/apps/govquests-api/rails_app/config/routes.rb @@ -1,5 +1,4 @@ Rails.application.routes.draw do - mount RailsEventStore::Browser => "/res" if Rails.env.development? # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/apps/govquests-api/rails_app/db/migrate/20240927211319_create_accounts.rb b/apps/govquests-api/rails_app/db/migrate/20240927211319_create_accounts.rb deleted file mode 100644 index f457070..0000000 --- a/apps/govquests-api/rails_app/db/migrate/20240927211319_create_accounts.rb +++ /dev/null @@ -1,12 +0,0 @@ -class CreateAccounts < ActiveRecord::Migration[8.0] - def change - create_table :accounts do |t| - t.string :account_id - t.string :string - t.string :address - t.integer :chain_id - - t.timestamps - end - end -end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930172742_create_quests.rb b/apps/govquests-api/rails_app/db/migrate/20240930172742_create_quests.rb deleted file mode 100644 index 233140a..0000000 --- a/apps/govquests-api/rails_app/db/migrate/20240930172742_create_quests.rb +++ /dev/null @@ -1,9 +0,0 @@ -class CreateQuests < ActiveRecord::Migration[8.0] - def change - create_table :quests do |t| - t.string :quest_id - - t.timestamps - end - end -end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb index bb2cd4b..378908d 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb @@ -1,7 +1,7 @@ class CreateQuests < ActiveRecord::Migration[8.0] def change create_table :quests do |t| - t.string :quest_id, null: false, index: {unique: true} + t.string :quest_id, null: false, index: { unique: true } t.string :audience, null: false t.string :quest_type, null: false t.integer :duration, null: false diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb b/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb index a8588ef..afbdaf6 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb @@ -1,8 +1,8 @@ class CreateUsers < ActiveRecord::Migration[8.0] def change create_table :users do |t| - t.string :user_id, null: false, index: {unique: true} - t.string :email, null: false, index: {unique: true} + t.string :user_id, null: false, index: { unique: true } + t.string :email, null: false, index: { unique: true } t.string :user_type, null: false, default: "non_delegate" t.jsonb :settings, default: {} t.jsonb :wallets, default: [] @@ -28,6 +28,6 @@ def change end add_index :user_sessions, :user_id - add_index :user_rewards, [:user_id, :reward_id], unique: true + add_index :user_rewards, [ :user_id, :reward_id ], unique: true end end diff --git a/apps/govquests-api/rails_app/lib/configuration.rb b/apps/govquests-api/rails_app/lib/read_models_configuration.rb similarity index 82% rename from apps/govquests-api/rails_app/lib/configuration.rb rename to apps/govquests-api/rails_app/lib/read_models_configuration.rb index 8e7d40d..24c3cf0 100644 --- a/apps/govquests-api/rails_app/lib/configuration.rb +++ b/apps/govquests-api/rails_app/lib/read_models_configuration.rb @@ -1,7 +1,7 @@ require_relative "../../govquests/configuration" require_relative "../../infra/lib/infra" -class Configuration +class ReadModelsConfiguration def call(event_store, command_bus) enable_res_infra_event_linking(event_store) @@ -13,16 +13,14 @@ def call(event_store, command_bus) private - def enable_authentication_read_model(event_store) - ClientAuthentication::Configuration.new.call(event_store) + Authentication::ReadModelConfiguration.new.call(event_store) end def enable_quests_read_model(event_store) - Quests::Configuration.new.call(event_store) + Questing::ReadModelConfiguration.new.call(event_store) end - def enable_res_infra_event_linking(event_store) [ RailsEventStore::LinkByEventType.new, diff --git a/apps/govquests-api/rails_app/public/robots.txt b/apps/govquests-api/rails_app/public/robots.txt deleted file mode 100644 index c19f78a..0000000 --- a/apps/govquests-api/rails_app/public/robots.txt +++ /dev/null @@ -1 +0,0 @@ -# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file From 661aa5b2ec5a1fe78fe067ba0b10f6ea2fe71a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Wed, 2 Oct 2024 16:56:26 -0300 Subject: [PATCH 07/17] chore: remove unused vat test --- .../govquests-api/infra/test/vat_rate_test.rb | 18 --------------- .../notifications/configuration.rb | 14 ++++++++++++ .../notifications/create_notification.rb | 20 +++++++++++++++++ .../20240930201733_create_notifications.rb | 22 +++++++++++++++++++ 4 files changed, 56 insertions(+), 18 deletions(-) delete mode 100644 apps/govquests-api/infra/test/vat_rate_test.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/create_notification.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb diff --git a/apps/govquests-api/infra/test/vat_rate_test.rb b/apps/govquests-api/infra/test/vat_rate_test.rb deleted file mode 100644 index d1e7cdc..0000000 --- a/apps/govquests-api/infra/test/vat_rate_test.rb +++ /dev/null @@ -1,18 +0,0 @@ -require_relative "test_helper" - -module Infra - class VatRateTest < Minitest::Test - cover "Infra::Types::VatRate" - - def test_comparable - assert_equal(0, vat.new(code: '10', rate: 10.to_d) <=> vat.new(code: 'ten', rate: 10.to_d)) - assert_equal(1, vat.new(code: '11', rate: 11.to_d) <=> vat.new(code: 'ten', rate: 10.to_d)) - end - - private - - def vat - Infra::Types::VatRate - end - end -end \ No newline at end of file diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb b/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb new file mode 100644 index 0000000..a1615b7 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb @@ -0,0 +1,14 @@ +module Notifications + class NotificationReadModel < ActiveRecord + end + + class Configuration + def call(event_store, command_bus) + event_store.subscribe(CreateNotificationHandler, to: [Notifications::NotificationCreated]) + event_store.subscribe(ScheduleNotificationHandler, to: [Notifications::NotificationScheduled]) + event_store.subscribe(SendNotificationHandler, to: [Notifications::NotificationSent]) + event_store.subscribe(ReceiveNotificationHandler, to: [Notifications::NotificationReceived]) + event_store.subscribe(OpenNotificationHandler, to: [Notifications::NotificationOpened]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/create_notification.rb b/apps/govquests-api/rails_app/app/read_models/notifications/create_notification.rb new file mode 100644 index 0000000..5aecf51 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/create_notification.rb @@ -0,0 +1,20 @@ +# app/read_models/notifications/create_notification.rb +module Notifications + class CreateNotificationHandler + def call(event) + notification_id = event.data.fetch(:notification_id) + content = event.data[:content] + priority = event.data[:priority] + channel = event.data[:channel] + template_id = event.data[:template_id] + + Notification.find_or_create_by(notification_id: notification_id).update( + content: content, + priority: priority, + channel: channel, + template_id: template_id, + status: "Created" + ) + end + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb b/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb new file mode 100644 index 0000000..df7343c --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb @@ -0,0 +1,22 @@ +class CreateNotifications < ActiveRecord::Migration[8.0] + def change + create_table :notifications do |t| + t.string :notification_id, null: false, index: {unique: true} + t.string :content, null: false + t.string :priority, null: false + t.string :channel, null: false + t.string :template_id, null: true + t.string :status, default: "Pending" + t.datetime :scheduled_at, null: true + t.timestamps + end + + create_table :notification_templates do |t| + t.string :template_id, null: false, index: {unique: true} + t.string :name, null: false + t.text :content, null: false + t.string :content_type, null: false, default: "text/plain" + t.timestamps + end + end +end From 290e8228a249ee83ebc94f72302f9d1383f33385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Wed, 2 Oct 2024 17:27:23 -0300 Subject: [PATCH 08/17] chore: refactor authentication domain --- .../authentication/lib/authentication.rb | 4 ++ .../lib/authentication/commands.rb | 14 ++---- .../lib/authentication/events.rb | 17 +------ .../lib/authentication/on_user_commands.rb | 20 +++++--- .../authentication/lib/authentication/user.rb | 49 +++---------------- .../lib/authentication/value_objects.rb | 12 ----- .../authentication/on_add_user_reward.rb | 11 ----- .../authentication/on_create_user.rb | 10 ++-- .../authentication/on_log_user_activity.rb | 16 ------ .../authentication/on_user_logged_in.rb | 16 ++++++ .../read_model_configuration.rb | 18 ++++++- .../update_user_quest_progress.rb | 12 ----- 12 files changed, 67 insertions(+), 132 deletions(-) delete mode 100644 apps/govquests-api/govquests/authentication/lib/authentication/value_objects.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/on_add_user_reward.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/on_log_user_activity.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/on_user_logged_in.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb diff --git a/apps/govquests-api/govquests/authentication/lib/authentication.rb b/apps/govquests-api/govquests/authentication/lib/authentication.rb index e257baa..abed8e1 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication.rb @@ -11,6 +11,10 @@ def call(event_store, command_bus) command_bus.register(RegisterUser, command_handler) command_bus.register(UpdateQuestProgress, command_handler) command_bus.register(LogUserActivity, command_handler) + command_bus.register(ConnectWallet, command_handler) + command_bus.register(LogIn, command_handler) + command_bus.register(LogOut, command_handler) + command_bus.register(ExpireSession, command_handler) end end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb index 7f89ee0..92afffd 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb @@ -9,18 +9,10 @@ class RegisterUser < Infra::Command alias_method :aggregate_id, :user_id end - class UpdateQuestProgress < Infra::Command + class LogIn < Infra::Command attribute :user_id, Infra::Types::UUID - attribute :quest_id, Infra::Types::UUID - attribute :progress_measure, Infra::Types::Integer - - alias_method :aggregate_id, :user_id - end - - class LogUserActivity < Infra::Command - attribute :user_id, Infra::Types::UUID - attribute :action_type, Infra::Types::String - attribute :action_timestamp, Infra::Types::Time + attribute :session_token, Infra::Types::String + attribute :timestamp, Infra::Types::Time alias_method :aggregate_id, :user_id end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/events.rb b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb index ea7d0bf..9e40a21 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/events.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb @@ -3,11 +3,7 @@ class UserRegistered < Infra::Event attribute :user_id, Infra::Types::UUID attribute :email, Infra::Types::String.optional attribute :user_type, Infra::Types::String - end - - class WalletConnected < Infra::Event - attribute :user_id, Infra::Types::UUID - attribute :wallet_address, Infra::Types::String + attribute :address, Infra::Types::String attribute :chain_id, Infra::Types::Integer end @@ -16,15 +12,4 @@ class UserLoggedIn < Infra::Event attribute :session_token, Infra::Types::String attribute :timestamp, Infra::Types::Time end - - class UserLoggedOut < Infra::Event - attribute :user_id, Infra::Types::UUID - attribute :session_token, Infra::Types::String - end - - class SessionExpired < Infra::Event - attribute :user_id, Infra::Types::UUID - attribute :session_token, Infra::Types::String - attribute :expired_at, Infra::Types::Time - end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb b/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb index afa2e0c..12e4fec 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb @@ -8,13 +8,19 @@ def call(command) @repository.with_aggregate(User, command.aggregate_id) do |user| case command when RegisterUser - user.register(command.email, command.user_type) - when UpdateQuestProgress - user.update_quest_progress(command.quest_id, command.progress_measure) - when ClaimReward - user.claim_reward(command.reward_id) - when LogUserActivity - user.log_activity(command.action_type, command.action_timestamp) + user.register( + command.email, + command.user_type, + command.address, + command.chain_id + ) + when LogIn + user.log_in( + command.session_token, + command.timestamp + ) + else + raise "Unknown command: #{command.class}" end end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/user.rb b/apps/govquests-api/govquests/authentication/lib/authentication/user.rb index bc827f3..ddf4623 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/user.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/user.rb @@ -1,15 +1,16 @@ -# app/models/user.rb module Authentication class User include AggregateRoot AlreadyRegistered = Class.new(StandardError) + SessionNotFound = Class.new(StandardError) + InvalidSession = Class.new(StandardError) def initialize(id) @id = id @email = nil @user_type = "non_delegate" - @address = nil + @wallet_address = nil @chain_id = nil @settings = {} @wallets = [] @@ -19,21 +20,13 @@ def initialize(id) @claimed_rewards = [] end - def register(email, user_type, address, chain_id) + def register(email, user_type, wallet_address, chain_id) raise AlreadyRegistered if @registered apply UserRegistered.new(data: { user_id: @id, email: email, user_type: user_type, - address: address, - chain_id: chain_id - }) - end - - def connect_wallet(wallet_address, chain_id) - apply WalletConnected.new(data: { - user_id: @id, wallet_address: wallet_address, chain_id: chain_id }) @@ -47,51 +40,21 @@ def log_in(session_token, timestamp) }) end - def log_out(session_token) - apply UserLoggedOut.new(data: { - user_id: @id, - session_token: session_token - }) - end - - def expire_session(session_token, expired_at) - apply SessionExpired.new(data: { - user_id: @id, - session_token: session_token, - expired_at: expired_at - }) - end - private on UserRegistered do |event| @registered = true @email = event.data[:email] @user_type = event.data[:user_type] - @address = event.data[:address] + @wallet_address = event.data[:wallet_address] @chain_id = event.data[:chain_id] end - on WalletConnected do |event| - @wallets << { - wallet_address: event.data[:wallet_address], - chain_id: event.data[:chain_id] - } - end - on UserLoggedIn do |event| @sessions << { session_token: event.data[:session_token], - timestamp: event.data[:timestamp] + logged_in_at: event.data[:timestamp] } end - - on UserLoggedOut do |event| - @sessions.reject! { |s| s[:session_token] == event.data[:session_token] } - end - - on SessionExpired do |event| - @sessions.reject! { |s| s[:session_token] == event.data[:session_token] } - end end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/value_objects.rb b/apps/govquests-api/govquests/authentication/lib/authentication/value_objects.rb deleted file mode 100644 index 7f4e2c6..0000000 --- a/apps/govquests-api/govquests/authentication/lib/authentication/value_objects.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Authentication - class UserType < Dry::Struct - values :delegate, :non_delegate - end - - class EmailAddress < Dry::Struct - def initialize(value) - super - raise "Invalid email format" unless URI::MailTo::EMAIL_REGEXP.match?(value) - end - end -end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/on_add_user_reward.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_add_user_reward.rb deleted file mode 100644 index 9764649..0000000 --- a/apps/govquests-api/rails_app/app/read_models/authentication/on_add_user_reward.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Authentication - class OnAddUserReward - def call(event) - user_id = event.data.fetch(:user_id) - reward_id = event.data.fetch(:reward_id) - - user = User.find_by(user_id: user_id) - user&.update_column(:last_reward_id, reward_id) - end - end -end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb index 89efd58..e474612 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb @@ -3,12 +3,16 @@ module Authentication class OnCreateUser def call(event) user_id = event.data.fetch(:user_id) - email = event.data[:email] - user_type = event.data[:user_type] + email = event.data.fetch(:email) + user_type = event.data.fetch(:user_type) + wallet_address = event.data.fetch(:wallet_address) + chain_id = event.data.fetch(:chain_id) User.find_or_create_by(user_id: user_id).update( email: email, - user_type: user_type + user_type: user_type, + wallet_address: wallet_address, + chain_id: chain_id ) end end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/on_log_user_activity.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_log_user_activity.rb deleted file mode 100644 index 4171ec3..0000000 --- a/apps/govquests-api/rails_app/app/read_models/authentication/on_log_user_activity.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Authentication - class OnLogUserActivity - def call(event) - user_id = event.data.fetch(:user_id) - action_type = event.data.fetch(:action_type) - action_timestamp = event.data.fetch(:action_timestamp) - - user = User.find_by(user_id: user_id) - if user - user.activities ||= [] - user.activities << { action_type: action_type, timestamp: action_timestamp } - user.save - end - end - end -end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/on_user_logged_in.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_logged_in.rb new file mode 100644 index 0000000..b7823a2 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_logged_in.rb @@ -0,0 +1,16 @@ +module Authentication + class OnUserLoggedIn + def call(event) + user_id = event.data.fetch(:user_id) + session_token = event.data.fetch(:session_token) + timestamp = event.data.fetch(:timestamp) + + UserSessionReadModel.create( + user_id: user_id, + session_token: session_token, + logged_in_at: timestamp, + logged_out_at: nil + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb index d14004d..2692884 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb @@ -1,11 +1,27 @@ module Authentication class UserReadModel < ApplicationRecord self.table_name = "users" + + validates :user_id, presence: true, uniqueness: true + validates :email, presence: true, uniqueness: true, format: {with: URI::MailTo::EMAIL_REGEXP} + validates :user_type, presence: true, inclusion: {in: %w[delegate non_delegate]} + validates :address, presence: true + validates :chain_id, presence: true + end + + class SessionReadModel < ApplicationRecord + self.table_name = "user_sessions" + + validates :user_id, presence: true + validates :session_token, presence: true, uniqueness: true + validates :logged_in_at, presence: true + # logged_out_at can be nil for active sessions end class ReadModelConfiguration def call(event_store) - event_store.subscribe(OnCreateUser, to: [ Authentication::UserRegistered ]) + event_store.subscribe(OnCreateUser.new, to: [Authentication::UserRegistered]) + event_store.subscribe(OnUserLoggedIn.new, to: [Authentication::UserLoggedIn]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb b/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb deleted file mode 100644 index c62e02b..0000000 --- a/apps/govquests-api/rails_app/app/read_models/authentication/update_user_quest_progress.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Authentication - class UpdateUserQuestProgress - def call(event) - user_id = event.data.fetch(:user_id) - quest_id = event.data.fetch(:quest_id) - progress_measure = event.data.fetch(:progress_measure) - - user = User.find_by(user_id: user_id) - user&.update_columns(last_quest_id: quest_id, progress_measure: progress_measure) - end - end -end From 5a6abc2b6fbbb0ab96bbf85ae61bd14cb488fab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Thu, 3 Oct 2024 08:42:39 -0300 Subject: [PATCH 09/17] chore: add tests to authentication and questing minimal domains --- apps/govquests-api/Makefile | 58 ++++++++++++- .../authentication/lib/authentication.rb | 5 -- .../lib/authentication/commands.rb | 4 +- .../lib/authentication/events.rb | 2 +- .../lib/authentication/on_user_commands.rb | 2 +- .../authentication/lib/authentication/user.rb | 10 ++- .../authentication/test/register_user_test.rb | 35 -------- .../authentication/test/user_test.rb | 64 ++++++++++++++ .../questing/lib/questing/commands.rb | 2 +- .../govquests/questing/lib/questing/events.rb | 2 +- .../govquests/questing/lib/questing/quest.rb | 16 ++-- .../questing/lib/questing/value_objects.rb | 2 +- .../govquests/questing/test/quest_test.rb | 60 ++++++++++++++ .../authentication/on_user_logged_in.rb | 2 +- ...n_create_user.rb => on_user_registered.rb} | 7 +- .../read_model_configuration.rb | 10 +-- .../notifications/configuration.rb | 6 +- ...fication.rb => on_notification_created.rb} | 5 +- .../read_models/questing/on_quest_created.rb | 31 +++---- .../questing/on_quest_status_updated.rb | 11 --- .../app/views/layouts/application.html.erb | 15 ---- .../rails_app/config/database.yml | 12 +-- .../20240930201733_create_notifications.rb | 4 +- apps/govquests-api/rails_app/db/schema.rb | 80 +++++++++++++++--- .../test/client_authentication/create_test.rb | 35 -------- .../rails_app/test/quests/create_test.rb | 32 ------- .../rails_app/test/read_model_handler_test.rb | 17 ---- .../authentication/on_user_logged_in_test.rb | 30 +++++++ .../authentication/on_user_registered_test.rb | 62 ++++++++++++++ .../questing/on_quest_created_test.rb | 83 +++++++++++++++++++ 30 files changed, 473 insertions(+), 231 deletions(-) delete mode 100644 apps/govquests-api/govquests/authentication/test/register_user_test.rb create mode 100644 apps/govquests-api/govquests/authentication/test/user_test.rb create mode 100644 apps/govquests-api/govquests/questing/test/quest_test.rb rename apps/govquests-api/rails_app/app/read_models/authentication/{on_create_user.rb => on_user_registered.rb} (71%) rename apps/govquests-api/rails_app/app/read_models/notifications/{create_notification.rb => on_notification_created.rb} (71%) delete mode 100644 apps/govquests-api/rails_app/app/read_models/questing/on_quest_status_updated.rb delete mode 100644 apps/govquests-api/rails_app/app/views/layouts/application.html.erb delete mode 100644 apps/govquests-api/rails_app/test/client_authentication/create_test.rb delete mode 100644 apps/govquests-api/rails_app/test/quests/create_test.rb delete mode 100644 apps/govquests-api/rails_app/test/read_model_handler_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/authentication/on_user_logged_in_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb diff --git a/apps/govquests-api/Makefile b/apps/govquests-api/Makefile index 55c1b27..37a5bca 100644 --- a/apps/govquests-api/Makefile +++ b/apps/govquests-api/Makefile @@ -1,5 +1,57 @@ -prepare-dev: - docker-compose up -d +# Define the subdirectories where tests should be run +SUBDIRS := govquests/authentication govquests/questing infra rails_app + +.PHONY: test $(SUBDIRS) + +# Default target +all: test +# Run tests in all subdirectories +test: $(SUBDIRS) + +$(SUBDIRS): + @echo "Running tests in $@" + @$(MAKE) -C $@ test + +# Install dependencies in all subdirectories install: - bundle install \ No newline at end of file + @for dir in $(SUBDIRS); do \ + echo "Installing dependencies in $$dir"; \ + $(MAKE) -C $$dir install; \ + done + +# Run linter (assuming it's only applicable to rails_app) +lint: + @$(MAKE) -C rails_app lint + +# Run Sorbet type checking (assuming it's only applicable to rails_app) +sorbet: + @$(MAKE) -C rails_app sorbet + +# Run mutation testing (where applicable) +mutate: + @for dir in govquests/authentication govquests/questing infra; do \ + echo "Running mutation tests in $$dir"; \ + $(MAKE) -C $$dir mutate; \ + done + +# Prepare development environment +prepare-dev: + @$(MAKE) -C . prepare-dev + +# Clean up (you can define cleanup tasks here if needed) +clean: + @echo "Cleaning up..." + # Add cleanup commands here + +# Help target +help: + @echo "Available targets:" + @echo " test - Run all tests in subdirectories" + @echo " install - Install dependencies in all subdirectories" + @echo " lint - Run linter on rails_app" + @echo " sorbet - Run Sorbet type checking on rails_app" + @echo " mutate - Run mutation tests where applicable" + @echo " prepare-dev - Prepare development environment" + @echo " clean - Clean up (if defined)" + @echo " help - Show this help message" \ No newline at end of file diff --git a/apps/govquests-api/govquests/authentication/lib/authentication.rb b/apps/govquests-api/govquests/authentication/lib/authentication.rb index abed8e1..6250591 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication.rb @@ -9,12 +9,7 @@ class Configuration def call(event_store, command_bus) command_handler = OnUserCommands.new(event_store) command_bus.register(RegisterUser, command_handler) - command_bus.register(UpdateQuestProgress, command_handler) - command_bus.register(LogUserActivity, command_handler) - command_bus.register(ConnectWallet, command_handler) command_bus.register(LogIn, command_handler) - command_bus.register(LogOut, command_handler) - command_bus.register(ExpireSession, command_handler) end end end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb index 92afffd..c81f4eb 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/commands.rb @@ -1,10 +1,10 @@ module Authentication class RegisterUser < Infra::Command attribute :user_id, Infra::Types::UUID - attribute :address, Infra::Types::String - attribute :chain_id, Infra::Types::Integer attribute :email, Infra::Types::String.optional attribute :user_type, Infra::Types::String + attribute :wallet_address, Infra::Types::String + attribute :chain_id, Infra::Types::Integer alias_method :aggregate_id, :user_id end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/events.rb b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb index 9e40a21..9569766 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/events.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/events.rb @@ -3,7 +3,7 @@ class UserRegistered < Infra::Event attribute :user_id, Infra::Types::UUID attribute :email, Infra::Types::String.optional attribute :user_type, Infra::Types::String - attribute :address, Infra::Types::String + attribute :wallet_address, Infra::Types::String attribute :chain_id, Infra::Types::Integer end diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb b/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb index 12e4fec..f7aa134 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/on_user_commands.rb @@ -11,7 +11,7 @@ def call(command) user.register( command.email, command.user_type, - command.address, + command.wallet_address, command.chain_id ) when LogIn diff --git a/apps/govquests-api/govquests/authentication/lib/authentication/user.rb b/apps/govquests-api/govquests/authentication/lib/authentication/user.rb index ddf4623..8492a59 100644 --- a/apps/govquests-api/govquests/authentication/lib/authentication/user.rb +++ b/apps/govquests-api/govquests/authentication/lib/authentication/user.rb @@ -10,8 +10,6 @@ def initialize(id) @id = id @email = nil @user_type = "non_delegate" - @wallet_address = nil - @chain_id = nil @settings = {} @wallets = [] @sessions = [] @@ -33,6 +31,8 @@ def register(email, user_type, wallet_address, chain_id) end def log_in(session_token, timestamp) + raise "User not registered" unless @registered + apply UserLoggedIn.new(data: { user_id: @id, session_token: session_token, @@ -46,8 +46,10 @@ def log_in(session_token, timestamp) @registered = true @email = event.data[:email] @user_type = event.data[:user_type] - @wallet_address = event.data[:wallet_address] - @chain_id = event.data[:chain_id] + @wallets << { + wallet_address: event.data[:wallet_address], + chain_id: event.data[:chain_id] + } end on UserLoggedIn do |event| diff --git a/apps/govquests-api/govquests/authentication/test/register_user_test.rb b/apps/govquests-api/govquests/authentication/test/register_user_test.rb deleted file mode 100644 index b2a11a0..0000000 --- a/apps/govquests-api/govquests/authentication/test/register_user_test.rb +++ /dev/null @@ -1,35 +0,0 @@ -require_relative "test_helper" - -module Authentication - class RegisterUserTest < Test - cover "Authentication*" - - def setup - @user_id = SecureRandom.uuid - @email = "user@example.com" - @chain_id = 1 - @user_type = "delegate" - @data = {user_id: @user_id, email: @email, user_type: @user_type} - end - - def test_user_should_get_registered - user_registered = UserRegistered.new(data: @data) - assert_events("Authentication::User$#{@user_id}", user_registered) do - register_user(@user_id, @email, @chain_id, @user_type) - end - end - - def test_should_not_allow_double_registration - assert_raises(User::AlreadyRegistered) do - register_user(@user_id, @email, @chain_id, @user_type) - register_user(@user_id, @email, @chain_id, @user_type) - end - end - - private - - def register_user(user_id, email, chain_id, user_type) - run_command(RegisterUser.new(user_id: user_id, address: "0x123", chain_id: chain_id, email: email, user_type: user_type)) - end - end -end diff --git a/apps/govquests-api/govquests/authentication/test/user_test.rb b/apps/govquests-api/govquests/authentication/test/user_test.rb new file mode 100644 index 0000000..733b73c --- /dev/null +++ b/apps/govquests-api/govquests/authentication/test/user_test.rb @@ -0,0 +1,64 @@ +require_relative "test_helper" + +module Authentication + class UserTest < Test + cover "Authentication::User" + + def setup + super + @user_id = SecureRandom.uuid + @user = User.new(@user_id) + end + + def test_register_a_new_user + email = "test@example.com" + user_type = "delegate" + wallet_address = "0x1234567890abcdef" + chain_id = 1 + + @user.register(email, user_type, wallet_address, chain_id) + + assert_equal 1, @user.unpublished_events.size + event = @user.unpublished_events.first + assert_instance_of UserRegistered, event + assert_equal @user_id, event.data[:user_id] + assert_equal email, event.data[:email] + assert_equal user_type, event.data[:user_type] + assert_equal wallet_address, event.data[:wallet_address] + assert_equal chain_id, event.data[:chain_id] + end + + def test_cannot_register_an_already_registered_user + @user.register("test@example.com", "delegate", "0x1234567890abcdef", 1) + + assert_raises(User::AlreadyRegistered) do + @user.register("new@example.com", "non_delegate", "0x0987654321fedcba", 2) + end + end + + def test_log_in_a_registered_user + @user.register("test@example.com", "delegate", "0x1234567890abcdef", 1) + session_token = SecureRandom.hex(16) + timestamp = Time.now + + @user.log_in(session_token, timestamp) + + assert_equal 2, @user.unpublished_events.size + event = @user.unpublished_events.to_a.last + assert_instance_of UserLoggedIn, event + assert_equal @user_id, event.data[:user_id] + assert_equal session_token, event.data[:session_token] + assert_equal timestamp, event.data[:timestamp] + end + + def test_cannot_log_in_an_unregistered_user + session_token = SecureRandom.hex(16) + timestamp = Time.now + + error = assert_raises(RuntimeError) do + @user.log_in(session_token, timestamp) + end + assert_equal "User not registered", error.message + end + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands.rb b/apps/govquests-api/govquests/questing/lib/questing/commands.rb index 7dde1ae..d5fb1d7 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/commands.rb @@ -2,7 +2,7 @@ module Questing class CreateQuest < Infra::Command attribute :quest_id, Infra::Types::UUID attribute :audience, Infra::Types::String - attribute :type, Infra::Types::String + attribute :quest_type, Infra::Types::String attribute :duration, Infra::Types::Integer attribute :difficulty, Infra::Types::String attribute :requirements, Infra::Types::Array.optional diff --git a/apps/govquests-api/govquests/questing/lib/questing/events.rb b/apps/govquests-api/govquests/questing/lib/questing/events.rb index a2cb4f4..ba98e71 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/events.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/events.rb @@ -2,7 +2,7 @@ module Questing class QuestCreated < Infra::Event attribute :quest_id, Infra::Types::UUID attribute :audience, Infra::Types::String - attribute :type, Infra::Types::String + attribute :quest_type, Infra::Types::String attribute :duration, Infra::Types::Integer attribute :difficulty, Infra::Types::String attribute :requirements, Infra::Types::Array.optional diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest.rb b/apps/govquests-api/govquests/questing/lib/questing/quest.rb index 514e00c..023b456 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/quest.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/quest.rb @@ -1,19 +1,10 @@ -# app/models/quest.rb module Questing class Quest include AggregateRoot def initialize(id) @id = id - @audience = nil - @quest_type = nil - @duration = nil - @difficulty = nil - @requirements = [] - @reward = {} - @subquests = [] @status = "created" - @progress = {} end def create(audience, quest_type, duration, difficulty, requirements = [], reward = {}, subquests = []) @@ -32,7 +23,10 @@ def create(audience, quest_type, duration, difficulty, requirements = [], reward end def associate_action(action_id) - apply ActionAssociatedWithQuest.new(data: {quest_id: @id, action_id: action_id}) + apply ActionAssociatedWithQuest.new(data: { + quest_id: @id, + action_id: action_id + }) end private @@ -45,7 +39,7 @@ def associate_action(action_id) @requirements = event.data[:requirements] @reward = event.data[:reward] @subquests = event.data[:subquests] - @status = "created" + @status = "active" # Change status to prevent re-creation end on ActionAssociatedWithQuest do |event| diff --git a/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb b/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb index ffa156e..20b24e9 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb @@ -21,7 +21,7 @@ class QuestReward < Dry::Struct end class QuestRequirement < Dry::Struct - attribute :type, Infra::Types::String + attribute :quest_type, Infra::Types::String attribute :description, Infra::Types::String end end diff --git a/apps/govquests-api/govquests/questing/test/quest_test.rb b/apps/govquests-api/govquests/questing/test/quest_test.rb new file mode 100644 index 0000000..ee7fd00 --- /dev/null +++ b/apps/govquests-api/govquests/questing/test/quest_test.rb @@ -0,0 +1,60 @@ +require_relative "test_helper" + +module Questing + class QuestTest < Test + cover "Questing::Quest" + + def setup + super + @quest_id = SecureRandom.uuid + @quest = Quest.new(@quest_id) + end + + def test_create_a_new_quest + audience = "AllUsers" + quest_type = "Standard" + duration = 7 + difficulty = "Medium" + requirements = [{quest_type: "action", description: "Complete action X"}] + reward = {quest_type: "points", value: 100} + subquests = [{id: SecureRandom.uuid, description: "Subquest 1"}] + + @quest.create(audience, quest_type, duration, difficulty, requirements, reward, subquests) + + events = @quest.unpublished_events.to_a + assert_equal 1, events.size + event = events.first + assert_instance_of QuestCreated, event + assert_equal @quest_id, event.data[:quest_id] + assert_equal audience, event.data[:audience] + assert_equal quest_type, event.data[:quest_type] + assert_equal duration, event.data[:duration] + assert_equal difficulty, event.data[:difficulty] + assert_equal requirements, event.data[:requirements] + assert_equal reward, event.data[:reward] + assert_equal subquests, event.data[:subquests] + end + + def test_cannot_create_an_already_created_quest + @quest.create("AllUsers", "Standard", 7, "Medium") + + assert_raises(RuntimeError, "Quest already created") do + @quest.create("Delegates", "Epic", 30, "Hard") + end + end + + def test_associate_an_action_with_a_quest + @quest.create("AllUsers", "Standard", 7, "Medium") + action_id = SecureRandom.uuid + + @quest.associate_action(action_id) + + events = @quest.unpublished_events.to_a + assert_equal 2, events.size + event = events.last + assert_instance_of ActionAssociatedWithQuest, event + assert_equal @quest_id, event.data[:quest_id] + assert_equal action_id, event.data[:action_id] + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/on_user_logged_in.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_logged_in.rb index b7823a2..37a0d21 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/on_user_logged_in.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_logged_in.rb @@ -5,7 +5,7 @@ def call(event) session_token = event.data.fetch(:session_token) timestamp = event.data.fetch(:timestamp) - UserSessionReadModel.create( + SessionReadModel.create( user_id: user_id, session_token: session_token, logged_in_at: timestamp, diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb similarity index 71% rename from apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb rename to apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb index e474612..f5f9444 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/on_create_user.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb @@ -1,6 +1,6 @@ # app/read_models/client_authentication/create_user.rb module Authentication - class OnCreateUser + class OnUserRegistered def call(event) user_id = event.data.fetch(:user_id) email = event.data.fetch(:email) @@ -8,11 +8,10 @@ def call(event) wallet_address = event.data.fetch(:wallet_address) chain_id = event.data.fetch(:chain_id) - User.find_or_create_by(user_id: user_id).update( + UserReadModel.find_or_create_by(user_id: user_id).update( email: email, user_type: user_type, - wallet_address: wallet_address, - chain_id: chain_id + wallets: [ { wallet_address: wallet_address, chain_id: chain_id } ] ) end end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb index 2692884..c614fcc 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb @@ -3,10 +3,8 @@ class UserReadModel < ApplicationRecord self.table_name = "users" validates :user_id, presence: true, uniqueness: true - validates :email, presence: true, uniqueness: true, format: {with: URI::MailTo::EMAIL_REGEXP} - validates :user_type, presence: true, inclusion: {in: %w[delegate non_delegate]} - validates :address, presence: true - validates :chain_id, presence: true + validates :email, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } + validates :user_type, presence: true, inclusion: { in: %w[delegate non_delegate] } end class SessionReadModel < ApplicationRecord @@ -20,8 +18,8 @@ class SessionReadModel < ApplicationRecord class ReadModelConfiguration def call(event_store) - event_store.subscribe(OnCreateUser.new, to: [Authentication::UserRegistered]) - event_store.subscribe(OnUserLoggedIn.new, to: [Authentication::UserLoggedIn]) + event_store.subscribe(OnUserRegistered.new, to: [ Authentication::UserRegistered ]) + event_store.subscribe(OnUserLoggedIn.new, to: [ Authentication::UserLoggedIn ]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb b/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb index a1615b7..c7c61eb 100644 --- a/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb @@ -4,11 +4,7 @@ class NotificationReadModel < ActiveRecord class Configuration def call(event_store, command_bus) - event_store.subscribe(CreateNotificationHandler, to: [Notifications::NotificationCreated]) - event_store.subscribe(ScheduleNotificationHandler, to: [Notifications::NotificationScheduled]) - event_store.subscribe(SendNotificationHandler, to: [Notifications::NotificationSent]) - event_store.subscribe(ReceiveNotificationHandler, to: [Notifications::NotificationReceived]) - event_store.subscribe(OpenNotificationHandler, to: [Notifications::NotificationOpened]) + event_store.subscribe(OnNotificationCreated, to: [ Notifications::NotificationCreated ]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/create_notification.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_created.rb similarity index 71% rename from apps/govquests-api/rails_app/app/read_models/notifications/create_notification.rb rename to apps/govquests-api/rails_app/app/read_models/notifications/on_notification_created.rb index 5aecf51..a934a26 100644 --- a/apps/govquests-api/rails_app/app/read_models/notifications/create_notification.rb +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_created.rb @@ -1,6 +1,5 @@ -# app/read_models/notifications/create_notification.rb module Notifications - class CreateNotificationHandler + class OnNotificationCreated def call(event) notification_id = event.data.fetch(:notification_id) content = event.data[:content] @@ -8,7 +7,7 @@ def call(event) channel = event.data[:channel] template_id = event.data[:template_id] - Notification.find_or_create_by(notification_id: notification_id).update( + NotificationReadModel.find_or_create_by(notification_id: notification_id).update( content: content, priority: priority, channel: channel, diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb index aa9b559..9ec503c 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb @@ -1,25 +1,18 @@ module Questing class OnQuestCreated def call(event) - quest_id = event.data.fetch(:quest_id) - audience = event.data.fetch(:audience) - quest_type = event.data.fetch(:quest_type) - duration = event.data.fetch(:duration) - difficulty = event.data.fetch(:difficulty) - requirements = event.data[:requirements] || [] - reward = event.data[:reward] || {} - subquests = event.data[:subquests] || [] - - Quest.find_or_create_by(quest_id: quest_id).update( - audience: audience, - quest_type: quest_type, - duration: duration, - difficulty: difficulty, - requirements: requirements, - reward: reward, - subquests: subquests, - status: "created" - ) + QuestReadModel.find_or_initialize_by(quest_id: event.data[:quest_id]).tap do |quest| + quest.update!( + audience: event.data[:audience], + quest_type: event.data[:quest_type], + duration: event.data[:duration], + difficulty: event.data[:difficulty], + requirements: event.data[:requirements], + reward: event.data[:reward], + subquests: event.data[:subquests], + status: "created" + ) + end end end end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_status_updated.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_status_updated.rb deleted file mode 100644 index 5a8c334..0000000 --- a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_status_updated.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Questing - class OnQuestStatusUpdated - def call(event) - quest_id = event.data.fetch(:quest_id) - status = event.data.fetch(:status) - - quest = Quest.find_by(quest_id: quest_id) - quest.update_column(:status, status) - end - end -end diff --git a/apps/govquests-api/rails_app/app/views/layouts/application.html.erb b/apps/govquests-api/rails_app/app/views/layouts/application.html.erb deleted file mode 100644 index d10b8b4..0000000 --- a/apps/govquests-api/rails_app/app/views/layouts/application.html.erb +++ /dev/null @@ -1,15 +0,0 @@ - - - - RailsApp - - <%= csrf_meta_tags %> - <%= csp_meta_tag %> - - <%= stylesheet_link_tag "application" %> - - - - <%= yield %> - - diff --git a/apps/govquests-api/rails_app/config/database.yml b/apps/govquests-api/rails_app/config/database.yml index 8e3c159..07a3302 100644 --- a/apps/govquests-api/rails_app/config/database.yml +++ b/apps/govquests-api/rails_app/config/database.yml @@ -19,7 +19,7 @@ default: &default # https://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> -development: +development: &development <<: *default database: govquests_api_development @@ -27,19 +27,19 @@ development: # To create additional roles in PostgreSQL see `$ createuser --help`. # When left blank, PostgreSQL will use the default role. This is # the same name as the operating system user running Rails. - #username: govquests_api + username: postgres # The password associated with the PostgreSQL role (username). - #password: + password: postgres # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. - #host: localhost + host: localhost # The TCP port the server listens on. Defaults to 5432. # If your server runs on a different port number, change accordingly. - #port: 5432 + port: 5432 # Schema search path. The server defaults to $user,public #schema_search_path: myapp,sharedapp,public @@ -54,7 +54,7 @@ development: # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - <<: *default + <<: *development database: govquests_api_test # As with config/credentials.yml, you never want to store sensitive information, diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb b/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb index df7343c..ec0e110 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb @@ -1,7 +1,7 @@ class CreateNotifications < ActiveRecord::Migration[8.0] def change create_table :notifications do |t| - t.string :notification_id, null: false, index: {unique: true} + t.string :notification_id, null: false, index: { unique: true } t.string :content, null: false t.string :priority, null: false t.string :channel, null: false @@ -12,7 +12,7 @@ def change end create_table :notification_templates do |t| - t.string :template_id, null: false, index: {unique: true} + t.string :template_id, null: false, index: { unique: true } t.string :name, null: false t.text :content, null: false t.string :content_type, null: false, default: "text/plain" diff --git a/apps/govquests-api/rails_app/db/schema.rb b/apps/govquests-api/rails_app/db/schema.rb index 4804a7c..f96458f 100644 --- a/apps/govquests-api/rails_app/db/schema.rb +++ b/apps/govquests-api/rails_app/db/schema.rb @@ -10,15 +10,9 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_09_30_172742) do - create_table "accounts", force: :cascade do |t| - t.string "account_id" - t.string "string" - t.string "address" - t.integer "chain_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end +ActiveRecord::Schema[8.0].define(version: 2024_09_30_201737) do + # These are extensions that must be enabled in order to support this database + enable_extension "pg_catalog.plpgsql" create_table "event_store_events", force: :cascade do |t| t.string "event_id", limit: 36, null: false @@ -44,10 +38,76 @@ t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true end + create_table "notification_templates", force: :cascade do |t| + t.string "template_id", null: false + t.string "name", null: false + t.text "content", null: false + t.string "content_type", default: "text/plain", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["template_id"], name: "index_notification_templates_on_template_id", unique: true + end + + create_table "notifications", force: :cascade do |t| + t.string "notification_id", null: false + t.string "content", null: false + t.string "priority", null: false + t.string "channel", null: false + t.string "template_id" + t.string "status", default: "Pending" + t.datetime "scheduled_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["notification_id"], name: "index_notifications_on_notification_id", unique: true + end + create_table "quests", force: :cascade do |t| - t.string "quest_id" + t.string "quest_id", null: false + t.string "audience", null: false + t.string "quest_type", null: false + t.integer "duration", null: false + t.string "difficulty", null: false + t.jsonb "requirements", default: [] + t.jsonb "reward", default: {} + t.jsonb "subquests", default: [] + t.string "status", default: "created" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["quest_id"], name: "index_quests_on_quest_id", unique: true + end + + create_table "user_rewards", force: :cascade do |t| + t.string "user_id", null: false + t.string "reward_id", null: false + t.string "status", default: "pending" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id", "reward_id"], name: "index_user_rewards_on_user_id_and_reward_id", unique: true + end + + create_table "user_sessions", force: :cascade do |t| + t.string "user_id", null: false + t.string "session_token", null: false + t.datetime "logged_in_at", null: false + t.datetime "logged_out_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_user_sessions_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.string "user_id", null: false + t.string "email", null: false + t.string "user_type", default: "non_delegate", null: false + t.jsonb "settings", default: {} + t.jsonb "wallets", default: [] + t.jsonb "sessions", default: [] + t.jsonb "quests_progress", default: {} + t.jsonb "activity_log", default: [] t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["user_id"], name: "index_users_on_user_id", unique: true end add_foreign_key "event_store_events_in_streams", "event_store_events", column: "event_id", primary_key: "event_id" diff --git a/apps/govquests-api/rails_app/test/client_authentication/create_test.rb b/apps/govquests-api/rails_app/test/client_authentication/create_test.rb deleted file mode 100644 index 4f3b2ac..0000000 --- a/apps/govquests-api/rails_app/test/client_authentication/create_test.rb +++ /dev/null @@ -1,35 +0,0 @@ -require "test_helper" - -module ClientAuthentication - class CreateTest < InMemoryTestCase - cover "Authentication*" - - def setup - super - ClientAuthentication::User.destroy_all - end - - def test_set_create - user_id = SecureRandom.uuid - address = "0x" - chain_id = 1 - - run_command( - Authentication::RegisterUser.new( - user_id: user_id, - address: address, - chain_id: chain_id - ) - ) - - user = ClientAuthentication::User.find_by(user_id: user_id, address: address, chain_id: chain_id) - assert user.present? - end - - private - - def event_store - Rails.configuration.event_store - end - end -end diff --git a/apps/govquests-api/rails_app/test/quests/create_test.rb b/apps/govquests-api/rails_app/test/quests/create_test.rb deleted file mode 100644 index 88d17d9..0000000 --- a/apps/govquests-api/rails_app/test/quests/create_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require "test_helper" - -module Quests - class CreateTest < InMemoryTestCase - cover "Questing*" - - def setup - super - Quests::Quest.destroy_all - end - - def test_set_create - quest_id = SecureRandom.uuid - - - run_command( - Questing::RegisterQuest.new( - quest_id: quest_id, - ) - ) - - account = Quests::Quest.find_by(quest_id: quest_id) - assert account.present? - end - - private - - def event_store - Rails.configuration.event_store - end - end -end diff --git a/apps/govquests-api/rails_app/test/read_model_handler_test.rb b/apps/govquests-api/rails_app/test/read_model_handler_test.rb deleted file mode 100644 index 3d22e37..0000000 --- a/apps/govquests-api/rails_app/test/read_model_handler_test.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "test_helper" - -class ReadModelHandlerTest < InMemoryTestCase - cover "ReadModelHandler*" - cover "CreateRecord*" - cover "CopyEventAttribute*" - - private - - def read_model - SingleTableReadModel.new(event_store, PublicOffer::Product, :product_id) - end - - def event_store - Rails.configuration.event_store - end -end diff --git a/apps/govquests-api/rails_app/test/read_models/authentication/on_user_logged_in_test.rb b/apps/govquests-api/rails_app/test/read_models/authentication/on_user_logged_in_test.rb new file mode 100644 index 0000000..5aeae6d --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/authentication/on_user_logged_in_test.rb @@ -0,0 +1,30 @@ +require "test_helper" + +module Authentication + class OnUserLoggedInTest < ActiveSupport::TestCase + def setup + @handler = Authentication::OnUserLoggedIn.new + @user_id = SecureRandom.uuid + @session_token = SecureRandom.hex(16) + @timestamp = Time.current + end + + test "creates a new user session when handling UserLoggedIn event" do + event = UserLoggedIn.new(data: { + user_id: @user_id, + session_token: @session_token, + timestamp: @timestamp + }) + + assert_difference "SessionReadModel.count", 1 do + @handler.call(event) + end + + session = ::Authentication::SessionReadModel.last + assert_equal @user_id, session.user_id + assert_equal @session_token, session.session_token + assert_equal @timestamp, session.logged_in_at + assert_nil session.logged_out_at + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb b/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb new file mode 100644 index 0000000..b933d75 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb @@ -0,0 +1,62 @@ +require "test_helper" + +module Authentication + class OnUserRegisteredTest < ActiveSupport::TestCase + def setup + @handler = Authentication::OnUserRegistered.new + @user_id = SecureRandom.uuid + @email = "test@example.com" + @user_type = "non_delegate" + @wallet_address = "0x1234567890abcdef" + @chain_id = 1 + end + + test "creates a new user when handling UserRegistered event" do + event = UserRegistered.new(data: { + user_id: @user_id, + email: @email, + user_type: @user_type, + wallet_address: @wallet_address, + chain_id: @chain_id + }) + + assert_difference -> { UserReadModel.count }, 1 do + @handler.call(event) + end + + user = UserReadModel.find_by(user_id: @user_id) + assert_equal @email, user.email + assert_equal @user_type, user.user_type + assert_equal @wallet_address, user.wallets.first["wallet_address"] + assert_equal @chain_id, user.wallets.first["chain_id"] + end + + test "updates existing user when handling UserRegistered event" do + existing_user = UserReadModel.new( + user_id: @user_id, + email: "old@example.com", + user_type: "delegate", + wallets: [ { wallet_address: "0x0987654321fedcba", chain_id: 2 } ] + ) + existing_user.save! + + event = UserRegistered.new(data: { + user_id: @user_id, + email: @email, + user_type: @user_type, + wallet_address: @wallet_address, + chain_id: @chain_id + }) + + assert_no_difference -> { UserReadModel.count } do + @handler.call(event) + end + + existing_user.reload + assert_equal @email, existing_user.email + assert_equal @user_type, existing_user.user_type + assert_equal @wallet_address, existing_user.wallets.first["wallet_address"] + assert_equal @chain_id, existing_user.wallets.first["chain_id"] + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb new file mode 100644 index 0000000..c7f2a60 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb @@ -0,0 +1,83 @@ +require "test_helper" + +module Questing + class OnQuestCreatedTest < ActiveSupport::TestCase + def setup + @handler = Questing::OnQuestCreated.new + @quest_id = SecureRandom.uuid + @audience = "AllUsers" + @quest_type = "Standard" + @duration = 7 + @difficulty = "Medium" + @requirements = [ { "quest_type" => "action", "description" => "Complete action X" } ] + @reward = { "quest_type" => "points", "value" => 100 } + @subquests = [ { "id" => SecureRandom.uuid, "description" => "Subquest 1" } ] + end + + test "creates a new quest when handling QuestCreated event" do + event = QuestCreated.new(data: { + quest_id: @quest_id, + audience: @audience, + quest_type: @quest_type, + duration: @duration, + difficulty: @difficulty, + requirements: @requirements, + reward: @reward, + subquests: @subquests + }) + + assert_difference -> { QuestReadModel.count }, 1 do + @handler.call(event) + end + + quest = QuestReadModel.find_by(quest_id: @quest_id) + assert_equal @audience, quest.audience + assert_equal @quest_type, quest.quest_type + assert_equal @duration, quest.duration + assert_equal @difficulty, quest.difficulty + assert_equal @requirements, quest.requirements + assert_equal @reward, quest.reward + assert_equal @subquests, quest.subquests + assert_equal "created", quest.status + end + + test "updates existing quest when handling QuestCreated event" do + existing_quest = QuestReadModel.create!( + quest_id: @quest_id, + audience: "Delegates", + quest_type: "Epic", + duration: 30, + difficulty: "Hard", + requirements: [], + reward: {}, + subquests: [], + status: "archived" + ) + + event = QuestCreated.new(data: { + quest_id: @quest_id, + audience: @audience, + quest_type: @quest_type, + duration: @duration, + difficulty: @difficulty, + requirements: @requirements, + reward: @reward, + subquests: @subquests + }) + + assert_no_difference -> { QuestReadModel.count } do + @handler.call(event) + end + + existing_quest.reload + assert_equal @audience, existing_quest.audience + assert_equal @quest_type, existing_quest.quest_type + assert_equal @duration, existing_quest.duration + assert_equal @difficulty, existing_quest.difficulty + assert_equal @requirements, existing_quest.requirements + assert_equal @reward, existing_quest.reward + assert_equal @subquests, existing_quest.subquests + assert_equal "created", existing_quest.status + end + end +end From 7f4b93fcc1f5c0b4dfb31bfa3b19c400bdc5fd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Thu, 3 Oct 2024 18:01:35 -0300 Subject: [PATCH 10/17] feat: add notifications, rewards, gamification domains --- .../govquests/action_tracking/.mutant.yml | 11 ++ .../govquests/action_tracking/Gemfile | 4 + .../govquests/action_tracking/Gemfile.lock | 110 ++++++++++++++++++ .../govquests/action_tracking/Makefile | 10 ++ .../govquests/action_tracking/README.md | 7 ++ .../action_tracking/lib/action_tracking.rb | 15 +++ .../lib/action_tracking/action.rb | 45 +++++++ .../lib/action_tracking/commands.rb | 18 +++ .../lib/action_tracking/events.rb | 14 +++ .../lib/action_tracking/on_action_commands.rb | 18 +++ .../lib/action_tracking/value_objects.rb | 10 ++ .../action_tracking/test/action_test.rb | 46 ++++++++ .../action_tracking/test/test_helper.rb | 19 +++ .../govquests/base_event_handler.rb | 46 ++++++++ apps/govquests-api/govquests/configuration.rb | 17 ++- .../govquests/gamification/.mutant.yml | 11 ++ .../govquests/gamification/Gemfile | 4 + .../govquests/gamification/Gemfile.lock | 110 ++++++++++++++++++ .../govquests/gamification/Makefile | 10 ++ .../govquests/gamification/README.md | 7 ++ .../gamification/lib/gamification.rb | 19 +++ .../gamification/lib/gamification/commands.rb | 45 +++++++ .../gamification/lib/gamification/events.rb | 33 ++++++ .../lib/gamification/game_profile.rb | 72 ++++++++++++ .../lib/gamification/leaderboard.rb | 25 ++++ .../gamification/on_game_profile_commands.rb | 31 +++++ .../lib/gamification/value_objects.rb | 9 ++ .../gamification/test/game_profile_test.rb | 39 +++++++ .../gamification/test/test_helper.rb | 19 +++ .../govquests/notifications/.mutant.yml | 11 ++ .../govquests/notifications/Gemfile | 4 + .../govquests/notifications/Gemfile.lock | 110 ++++++++++++++++++ .../govquests/notifications/Makefile | 10 ++ .../govquests/notifications/README.md | 7 ++ .../notifications/lib/notifications.rb | 18 +++ .../lib/notifications/commands.rb | 22 ++++ .../notifications/lib/notifications/events.rb | 33 ++++++ .../lib/notifications/notification.rb | 53 +++++++++ .../notifications/notification_template.rb | 54 +++++++++ .../notifications/on_notification_commands.rb | 20 ++++ .../lib/notifications/on_template_commands.rb | 24 ++++ .../lib/notifications/template_commands.rb | 25 ++++ .../lib/notifications/template_events.rb | 19 +++ .../lib/notifications/value_objects.rb | 71 +++++++++++ .../notifications/test/notification_test.rb | 57 +++++++++ .../notifications/test/test_helper.rb | 19 +++ .../govquests/questing/lib/questing/events.rb | 5 + .../lib/questing/on_quest_commands.rb | 3 +- .../govquests/rewarding/.mutant.yml | 11 ++ .../govquests-api/govquests/rewarding/Gemfile | 4 + .../govquests/rewarding/Gemfile.lock | 110 ++++++++++++++++++ .../govquests/rewarding/Makefile | 10 ++ .../govquests/rewarding/README.md | 7 ++ .../govquests/rewarding/lib/rewarding.rb | 16 +++ .../rewarding/lib/rewarding/commands.rb | 24 ++++ .../rewarding/lib/rewarding/events.rb | 26 +++++ .../lib/rewarding/on_reward_commands.rb | 20 ++++ .../rewarding/lib/rewarding/reward.rb | 50 ++++++++ .../rewarding/lib/rewarding/value_objects.rb | 21 ++++ .../govquests/rewarding/test/reward_test.rb | 69 +++++++++++ .../govquests/rewarding/test/test_helper.rb | 13 +++ apps/govquests-api/rails_app/.kamal/secrets | 2 +- .../action_tracking/on_action_created.rb | 12 ++ .../action_tracking/on_action_executed.rb | 19 +++ .../read_model_configuration.rb | 29 +++++ .../authentication/on_user_registered.rb | 11 +- .../gamification/on_badge_earned.rb | 21 ++++ .../gamification/on_leaderboard_updated.rb | 28 +++++ .../gamification/on_streak_maintained.rb | 16 +++ .../gamification/on_tier_achieved.rb | 16 +++ .../gamification/on_track_completed.rb | 16 +++ .../gamification/read_model_configuration.rb | 44 +++++++ .../notifications/configuration.rb | 10 -- .../notifications/on_notification_created.rb | 18 +-- .../notifications/on_notification_opened.rb | 11 ++ .../notifications/on_notification_received.rb | 11 ++ .../on_notification_scheduled.rb | 11 ++ .../notifications/on_notification_sent.rb | 11 ++ .../notifications/on_template_created.rb | 16 +++ .../notifications/on_template_deleted.rb | 10 ++ .../notifications/on_template_updated.rb | 19 +++ .../notifications/read_model_configuration.rb | 37 ++++++ .../questing/on_user_started_quest.rb | 13 +++ .../questing/read_model_configuration.rb | 5 + .../rewarding/on_reward_claimed.rb | 12 ++ .../rewarding/on_reward_created.rb | 17 +++ .../rewarding/on_reward_expired.rb | 12 ++ .../rewarding/on_reward_inventory_depleted.rb | 12 ++ .../read_models/rewarding/on_reward_issued.rb | 12 ++ .../rewarding/read_model_configuration.rb | 18 +++ .../read_models/single_table_read_model.rb | 54 ++++----- .../rails_app/config/storage.yml | 2 +- .../20240930201733_create_notifications.rb | 21 +++- .../migrate/20240930201744_create_rewards.rb | 14 +++ .../20240930201750_create_user_quests.rb | 15 +++ .../migrate/20240930201751_create_actions.rb | 22 ++++ ...0240930201752_create_user_game_profiles.rb | 30 +++++ apps/govquests-api/rails_app/db/schema.rb | 99 +++++++++++++++- .../lib/read_models_configuration.rb | 22 +++- .../action_tracking/on_action_created_test.rb | 32 +++++ .../on_action_executed_test.rb | 31 +++++ .../on_notification_created_test.rb | 41 +++++++ .../on_template_created_test.rb | 31 +++++ .../authentication/on_user_registered_test.rb | 24 ++++ .../gamification/on_badge_earned_test.rb | 44 +++++++ .../on_leaderboard_updated_test.rb | 37 ++++++ .../gamification/on_tier_achieved_test.rb | 27 +++++ .../questing/on_user_started_quest_test.rb | 26 +++++ .../rewarding/on_reward_claimed_test.rb | 34 ++++++ .../rewarding/on_reward_created_test.rb | 30 +++++ .../rails_app/test/test_helper.rb | 24 ++-- 111 files changed, 2811 insertions(+), 88 deletions(-) create mode 100644 apps/govquests-api/govquests/action_tracking/.mutant.yml create mode 100644 apps/govquests-api/govquests/action_tracking/Gemfile create mode 100644 apps/govquests-api/govquests/action_tracking/Gemfile.lock create mode 100644 apps/govquests-api/govquests/action_tracking/Makefile create mode 100644 apps/govquests-api/govquests/action_tracking/README.md create mode 100644 apps/govquests-api/govquests/action_tracking/lib/action_tracking.rb create mode 100644 apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb create mode 100644 apps/govquests-api/govquests/action_tracking/lib/action_tracking/commands.rb create mode 100644 apps/govquests-api/govquests/action_tracking/lib/action_tracking/events.rb create mode 100644 apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb create mode 100644 apps/govquests-api/govquests/action_tracking/lib/action_tracking/value_objects.rb create mode 100644 apps/govquests-api/govquests/action_tracking/test/action_test.rb create mode 100644 apps/govquests-api/govquests/action_tracking/test/test_helper.rb create mode 100644 apps/govquests-api/govquests/base_event_handler.rb create mode 100644 apps/govquests-api/govquests/gamification/.mutant.yml create mode 100644 apps/govquests-api/govquests/gamification/Gemfile create mode 100644 apps/govquests-api/govquests/gamification/Gemfile.lock create mode 100644 apps/govquests-api/govquests/gamification/Makefile create mode 100644 apps/govquests-api/govquests/gamification/README.md create mode 100644 apps/govquests-api/govquests/gamification/lib/gamification.rb create mode 100644 apps/govquests-api/govquests/gamification/lib/gamification/commands.rb create mode 100644 apps/govquests-api/govquests/gamification/lib/gamification/events.rb create mode 100644 apps/govquests-api/govquests/gamification/lib/gamification/game_profile.rb create mode 100644 apps/govquests-api/govquests/gamification/lib/gamification/leaderboard.rb create mode 100644 apps/govquests-api/govquests/gamification/lib/gamification/on_game_profile_commands.rb create mode 100644 apps/govquests-api/govquests/gamification/lib/gamification/value_objects.rb create mode 100644 apps/govquests-api/govquests/gamification/test/game_profile_test.rb create mode 100644 apps/govquests-api/govquests/gamification/test/test_helper.rb create mode 100644 apps/govquests-api/govquests/notifications/.mutant.yml create mode 100644 apps/govquests-api/govquests/notifications/Gemfile create mode 100644 apps/govquests-api/govquests/notifications/Gemfile.lock create mode 100644 apps/govquests-api/govquests/notifications/Makefile create mode 100644 apps/govquests-api/govquests/notifications/README.md create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/commands.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/events.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/notification.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/notification_template.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/on_notification_commands.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/on_template_commands.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/template_commands.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/template_events.rb create mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/value_objects.rb create mode 100644 apps/govquests-api/govquests/notifications/test/notification_test.rb create mode 100644 apps/govquests-api/govquests/notifications/test/test_helper.rb create mode 100644 apps/govquests-api/govquests/rewarding/.mutant.yml create mode 100644 apps/govquests-api/govquests/rewarding/Gemfile create mode 100644 apps/govquests-api/govquests/rewarding/Gemfile.lock create mode 100644 apps/govquests-api/govquests/rewarding/Makefile create mode 100644 apps/govquests-api/govquests/rewarding/README.md create mode 100644 apps/govquests-api/govquests/rewarding/lib/rewarding.rb create mode 100644 apps/govquests-api/govquests/rewarding/lib/rewarding/commands.rb create mode 100644 apps/govquests-api/govquests/rewarding/lib/rewarding/events.rb create mode 100644 apps/govquests-api/govquests/rewarding/lib/rewarding/on_reward_commands.rb create mode 100644 apps/govquests-api/govquests/rewarding/lib/rewarding/reward.rb create mode 100644 apps/govquests-api/govquests/rewarding/lib/rewarding/value_objects.rb create mode 100644 apps/govquests-api/govquests/rewarding/test/reward_test.rb create mode 100644 apps/govquests-api/govquests/rewarding/test/test_helper.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/gamification/on_leaderboard_updated.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/gamification/on_streak_maintained.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/gamification/on_tier_achieved.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/gamification/on_track_completed.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/gamification/read_model_configuration.rb delete mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/on_notification_opened.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/on_notification_received.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/on_notification_scheduled.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/on_notification_sent.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/on_template_created.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/on_template_deleted.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/on_template_updated.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/notifications/read_model_configuration.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/questing/on_user_started_quest.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_claimed.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_created.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_expired.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_inventory_depleted.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_issued.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20240930201750_create_user_quests.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20240930201752_create_user_game_profiles.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/action_tracking/on_notification_created_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/action_tracking/on_template_created_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/gamification/on_badge_earned_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/gamification/on_leaderboard_updated_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/gamification/on_tier_achieved_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/questing/on_user_started_quest_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_claimed_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_created_test.rb diff --git a/apps/govquests-api/govquests/action_tracking/.mutant.yml b/apps/govquests-api/govquests/action_tracking/.mutant.yml new file mode 100644 index 0000000..a4ea1c5 --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/.mutant.yml @@ -0,0 +1,11 @@ +requires: + - ./test/test_helper +integration: minitest +usage: opensource +coverage_criteria: + process_abort: true +matcher: + subjects: + - Questing* + ignore: + - Questing::Configuration#call diff --git a/apps/govquests-api/govquests/action_tracking/Gemfile b/apps/govquests-api/govquests/action_tracking/Gemfile new file mode 100644 index 0000000..00fef6d --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +eval_gemfile "../../infra/Gemfile.test" +gem "infra", path: "../../infra" diff --git a/apps/govquests-api/govquests/action_tracking/Gemfile.lock b/apps/govquests-api/govquests/action_tracking/Gemfile.lock new file mode 100644 index 0000000..d3c8cdd --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/Gemfile.lock @@ -0,0 +1,110 @@ +PATH + remote: ../../infra + specs: + infra (1.0.0) + aggregate_root (~> 2.15) + arkency-command_bus + dry-struct + dry-types + rake + ruby_event_store (~> 2.15) + ruby_event_store-transformations + +GEM + remote: https://oss:7AXfeZdAfCqL1PvHm2nvDJO6Zd9UW8IK@gem.mutant.dev/ + specs: + mutant-license (0.1.1.2.1627430819213747598431630701693729869473.6) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + aggregate_root (2.15.0) + ruby_event_store (= 2.15.0) + arkency-command_bus (0.4.1) + concurrent-ruby + ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.4) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + diff-lcs (1.5.1) + drb (2.2.0) + ruby2_keywords + dry-core (1.0.1) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.7.1) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + minitest (5.25.0) + mutant (0.12.4) + diff-lcs (~> 1.3) + parser (~> 3.3.0) + regexp_parser (~> 2.9.0) + sorbet-runtime (~> 0.5.0) + unparser (~> 0.6.14) + mutant-minitest (0.12.4) + minitest (~> 5.11) + mutant (= 0.12.4) + mutex_m (0.2.0) + parser (3.3.4.2) + ast (~> 2.4.1) + racc + racc (1.8.1) + rake (13.1.0) + regexp_parser (2.9.2) + ruby2_keywords (0.0.5) + ruby_event_store (2.15.0) + concurrent-ruby (~> 1.0, >= 1.1.6) + ruby_event_store-transformations (0.1.0) + activesupport (>= 5.0) + ruby_event_store (>= 2.0.0, < 3.0.0) + sorbet-runtime (0.5.11525) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unparser (0.6.15) + diff-lcs (~> 1.3) + parser (>= 3.3.0) + zeitwerk (2.6.12) + +PLATFORMS + arm64-darwin-20 + arm64-darwin-21 + ruby + x86_64-darwin-20 + x86_64-linux + +DEPENDENCIES + infra! + minitest (= 5.25.0)! + mutant-license! + mutant-minitest (= 0.12.4)! + +BUNDLED WITH + 2.5.9 diff --git a/apps/govquests-api/govquests/action_tracking/Makefile b/apps/govquests-api/govquests/action_tracking/Makefile new file mode 100644 index 0000000..23850fa --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/Makefile @@ -0,0 +1,10 @@ +install: + @bundle install + +test: + @bundle exec ruby -e "require \"rake/rake_test_loader\"" test/*_test.rb + +mutate: + @RAILS_ENV=test bundle exec mutant run + +.PHONY: install test mutate diff --git a/apps/govquests-api/govquests/action_tracking/README.md b/apps/govquests-api/govquests/action_tracking/README.md new file mode 100644 index 0000000..b187021 --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/README.md @@ -0,0 +1,7 @@ +# Questing + +#### Up and running + +``` +make install test mutate +``` diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking.rb new file mode 100644 index 0000000..0c5a62f --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking.rb @@ -0,0 +1,15 @@ +require "infra" +require_relative "action_tracking/commands" +require_relative "action_tracking/events" +require_relative "action_tracking/on_action_commands" +require_relative "action_tracking/action" + +module ActionTracking + class Configuration + def call(event_store, command_bus) + command_handler = OnActionCommands.new(event_store) + command_bus.register(CreateAction, command_handler) + command_bus.register(ExecuteAction, command_handler) + end + end +end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb new file mode 100644 index 0000000..c38239d --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb @@ -0,0 +1,45 @@ +module ActionTracking + class Action + include AggregateRoot + + def initialize(id) + @id = id + @content = nil + @priority = nil + @channel = nil + @executions = [] + end + + def create(content, priority, channel) + apply ActionCreated.new(data: { + action_id: @id, + content: content, + priority: priority, + channel: channel + }) + end + + def execute(user_id, timestamp) + apply ActionExecuted.new(data: { + action_id: @id, + user_id: user_id, + timestamp: timestamp + }) + end + + private + + on ActionCreated do |event| + @content = event.data[:content] + @priority = event.data[:priority] + @channel = event.data[:channel] + end + + on ActionExecuted do |event| + @executions << { + user_id: event.data[:user_id], + timestamp: event.data[:timestamp] + } + end + end +end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/commands.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/commands.rb new file mode 100644 index 0000000..3f2e0b2 --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/commands.rb @@ -0,0 +1,18 @@ +module ActionTracking + class CreateAction < Infra::Command + attribute :action_id, Infra::Types::UUID + attribute :content, Infra::Types::String + attribute :priority, Infra::Types::String + attribute :channel, Infra::Types::String + + alias_method :aggregate_id, :action_id + end + + class ExecuteAction < Infra::Command + attribute :action_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + attribute :timestamp, Infra::Types::Time + + alias_method :aggregate_id, :action_id + end +end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/events.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/events.rb new file mode 100644 index 0000000..4b808ba --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/events.rb @@ -0,0 +1,14 @@ +module ActionTracking + class ActionCreated < Infra::Event + attribute :action_id, Infra::Types::UUID + attribute :content, Infra::Types::String + attribute :priority, Infra::Types::String + attribute :channel, Infra::Types::String + end + + class ActionExecuted < Infra::Event + attribute :action_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + attribute :timestamp, Infra::Types::Time + end +end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb new file mode 100644 index 0000000..b6d408f --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb @@ -0,0 +1,18 @@ +module ActionTracking + class OnActionCommands + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + @repository.with_aggregate(Action, command.aggregate_id) do |action| + case command + when CreateAction + action.create(command.content, command.priority, command.channel) + when ExecuteAction + action.execute(command.user_id, command.timestamp) + end + end + end + end +end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/value_objects.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/value_objects.rb new file mode 100644 index 0000000..ec493cf --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/value_objects.rb @@ -0,0 +1,10 @@ +# govquests/action_tracking/lib/action_tracking/value_objects.rb +module ActionTracking + class ActionPriority < Dry::Struct + attribute :priority, Infra::Types::String.enum("Low", "Medium", "High") + end + + class ActionChannel < Dry::Struct + attribute :channel, Infra::Types::String.enum("Email", "SMS", "Push") + end +end diff --git a/apps/govquests-api/govquests/action_tracking/test/action_test.rb b/apps/govquests-api/govquests/action_tracking/test/action_test.rb new file mode 100644 index 0000000..b2d1967 --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/test/action_test.rb @@ -0,0 +1,46 @@ +require_relative "test_helper" + +module ActionTracking + class ActionTest < Test + cover "ActionTracking::Action" + + def setup + super + @action_id = SecureRandom.uuid + @action = Action.new(@action_id) + end + + def test_create_a_new_action + content = "Complete survey" + priority = "High" + channel = "Email" + + @action.create(content, priority, channel) + + events = @action.unpublished_events.to_a + assert_equal 1, events.size + event = events.first + assert_instance_of ActionCreated, event + assert_equal @action_id, event.data[:action_id] + assert_equal content, event.data[:content] + assert_equal priority, event.data[:priority] + assert_equal channel, event.data[:channel] + end + + def test_execute_an_action + @action.create("Complete survey", "High", "Email") + user_id = SecureRandom.uuid + timestamp = Time.now + + @action.execute(user_id, timestamp) + + events = @action.unpublished_events.to_a + assert_equal 2, events.size + event = events.last + assert_instance_of ActionExecuted, event + assert_equal @action_id, event.data[:action_id] + assert_equal user_id, event.data[:user_id] + assert_equal timestamp, event.data[:timestamp] + end + end +end diff --git a/apps/govquests-api/govquests/action_tracking/test/test_helper.rb b/apps/govquests-api/govquests/action_tracking/test/test_helper.rb new file mode 100644 index 0000000..a8fadc8 --- /dev/null +++ b/apps/govquests-api/govquests/action_tracking/test/test_helper.rb @@ -0,0 +1,19 @@ +require "minitest/autorun" +require "mutant/minitest/coverage" + +require_relative "../lib/action_tracking" + +module Questing + class Test < Infra::InMemoryTest + def before_setup + super + Configuration.new.call(event_store, command_bus) + end + + private + + def fake_login + "fake_login" + end + end +end diff --git a/apps/govquests-api/govquests/base_event_handler.rb b/apps/govquests-api/govquests/base_event_handler.rb new file mode 100644 index 0000000..23d5e5c --- /dev/null +++ b/apps/govquests-api/govquests/base_event_handler.rb @@ -0,0 +1,46 @@ +# govquests/lib/base_event_handler.rb + +module GovQuests + class BaseEventHandler + def initialize(event_store, resource_class = nil, id_field = :id) + @event_store = event_store + @resource_class = resource_class + @id_field = id_field + end + + def call(event) + # This should be overridden by subclasses to handle specific events + raise NotImplementedError, "Subclasses must implement the `call` method" + end + + protected + + def find_resource(event, resource_class = @resource_class, id_field = @id_field) + resource_id = event.data.fetch(id_field) + resource_class.find_or_initialize_by(id_field => resource_id) + end + + def update_resource(event, resource_class = @resource_class, id_field = @id_field) + resource = find_resource(event, resource_class, id_field) + yield(resource) if block_given? + resource.save! + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error "Failed to update resource: #{e.message}" + end + + def concurrent_safely(event) + resource_class = @resource_class.name + resource_id = event.data.fetch(@id_field) + + ApplicationRecord.with_advisory_lock(resource_class, resource_id) do + yield + end + rescue ActiveRecord::StatementInvalid => e + Rails.logger.error "Concurrency issue with event #{event.event_type}: #{e.message}" + end + + private + + attr_reader :event_store, :resource_class, :id_field + end +end diff --git a/apps/govquests-api/govquests/configuration.rb b/apps/govquests-api/govquests/configuration.rb index 7d2b760..b73829c 100644 --- a/apps/govquests-api/govquests/configuration.rb +++ b/apps/govquests-api/govquests/configuration.rb @@ -1,10 +1,14 @@ require_relative "authentication/lib/authentication" require_relative "questing/lib/questing" +require_relative "rewarding/lib/rewarding" +require_relative "notifications/lib/notifications" +require_relative "action_tracking/lib/action_tracking" +require_relative "gamification/lib/gamification" - -module Govquests +module GovQuests class Configuration - def initialize;end + def initialize + end def call(event_store, command_bus) configure_bounded_contexts(event_store, command_bus) @@ -14,8 +18,11 @@ def configure_bounded_contexts(event_store, command_bus) [ Authentication::Configuration.new, Questing::Configuration.new, - ].each { |c| c.call(event_store, command_bus) } + Rewarding::Configuration.new, + Notifications::Configuration.new, + ActionTracking::Configuration.new, + Gamification::Configuration.new + ].each { |context| context.call(event_store, command_bus) } end - end end diff --git a/apps/govquests-api/govquests/gamification/.mutant.yml b/apps/govquests-api/govquests/gamification/.mutant.yml new file mode 100644 index 0000000..a4ea1c5 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/.mutant.yml @@ -0,0 +1,11 @@ +requires: + - ./test/test_helper +integration: minitest +usage: opensource +coverage_criteria: + process_abort: true +matcher: + subjects: + - Questing* + ignore: + - Questing::Configuration#call diff --git a/apps/govquests-api/govquests/gamification/Gemfile b/apps/govquests-api/govquests/gamification/Gemfile new file mode 100644 index 0000000..00fef6d --- /dev/null +++ b/apps/govquests-api/govquests/gamification/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +eval_gemfile "../../infra/Gemfile.test" +gem "infra", path: "../../infra" diff --git a/apps/govquests-api/govquests/gamification/Gemfile.lock b/apps/govquests-api/govquests/gamification/Gemfile.lock new file mode 100644 index 0000000..d3c8cdd --- /dev/null +++ b/apps/govquests-api/govquests/gamification/Gemfile.lock @@ -0,0 +1,110 @@ +PATH + remote: ../../infra + specs: + infra (1.0.0) + aggregate_root (~> 2.15) + arkency-command_bus + dry-struct + dry-types + rake + ruby_event_store (~> 2.15) + ruby_event_store-transformations + +GEM + remote: https://oss:7AXfeZdAfCqL1PvHm2nvDJO6Zd9UW8IK@gem.mutant.dev/ + specs: + mutant-license (0.1.1.2.1627430819213747598431630701693729869473.6) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + aggregate_root (2.15.0) + ruby_event_store (= 2.15.0) + arkency-command_bus (0.4.1) + concurrent-ruby + ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.4) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + diff-lcs (1.5.1) + drb (2.2.0) + ruby2_keywords + dry-core (1.0.1) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.7.1) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + minitest (5.25.0) + mutant (0.12.4) + diff-lcs (~> 1.3) + parser (~> 3.3.0) + regexp_parser (~> 2.9.0) + sorbet-runtime (~> 0.5.0) + unparser (~> 0.6.14) + mutant-minitest (0.12.4) + minitest (~> 5.11) + mutant (= 0.12.4) + mutex_m (0.2.0) + parser (3.3.4.2) + ast (~> 2.4.1) + racc + racc (1.8.1) + rake (13.1.0) + regexp_parser (2.9.2) + ruby2_keywords (0.0.5) + ruby_event_store (2.15.0) + concurrent-ruby (~> 1.0, >= 1.1.6) + ruby_event_store-transformations (0.1.0) + activesupport (>= 5.0) + ruby_event_store (>= 2.0.0, < 3.0.0) + sorbet-runtime (0.5.11525) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unparser (0.6.15) + diff-lcs (~> 1.3) + parser (>= 3.3.0) + zeitwerk (2.6.12) + +PLATFORMS + arm64-darwin-20 + arm64-darwin-21 + ruby + x86_64-darwin-20 + x86_64-linux + +DEPENDENCIES + infra! + minitest (= 5.25.0)! + mutant-license! + mutant-minitest (= 0.12.4)! + +BUNDLED WITH + 2.5.9 diff --git a/apps/govquests-api/govquests/gamification/Makefile b/apps/govquests-api/govquests/gamification/Makefile new file mode 100644 index 0000000..23850fa --- /dev/null +++ b/apps/govquests-api/govquests/gamification/Makefile @@ -0,0 +1,10 @@ +install: + @bundle install + +test: + @bundle exec ruby -e "require \"rake/rake_test_loader\"" test/*_test.rb + +mutate: + @RAILS_ENV=test bundle exec mutant run + +.PHONY: install test mutate diff --git a/apps/govquests-api/govquests/gamification/README.md b/apps/govquests-api/govquests/gamification/README.md new file mode 100644 index 0000000..b187021 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/README.md @@ -0,0 +1,7 @@ +# Questing + +#### Up and running + +``` +make install test mutate +``` diff --git a/apps/govquests-api/govquests/gamification/lib/gamification.rb b/apps/govquests-api/govquests/gamification/lib/gamification.rb new file mode 100644 index 0000000..bc7a3cd --- /dev/null +++ b/apps/govquests-api/govquests/gamification/lib/gamification.rb @@ -0,0 +1,19 @@ +require "infra" +require_relative "gamification/commands" +require_relative "gamification/events" +require_relative "gamification/on_game_profile_commands" +require_relative "gamification/game_profile" + +module Gamification + class Configuration + def call(event_store, command_bus) + command_handler = OnGameProfileCommands.new(event_store) + command_bus.register(UpdateScore, command_handler) + command_bus.register(AchieveTier, command_handler) + command_bus.register(CompleteTrack, command_handler) + command_bus.register(MaintainStreak, command_handler) + command_bus.register(EarnBadge, command_handler) + command_bus.register(UpdateLeaderboard, command_handler) + end + end +end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/commands.rb b/apps/govquests-api/govquests/gamification/lib/gamification/commands.rb new file mode 100644 index 0000000..1d34706 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/lib/gamification/commands.rb @@ -0,0 +1,45 @@ +# govquests/gamification/lib/gamification/commands.rb +module Gamification + class UpdateScore < Infra::Command + attribute :profile_id, Infra::Types::UUID + attribute :points, Infra::Types::Integer + + alias_method :aggregate_id, :profile_id + end + + class AchieveTier < Infra::Command + attribute :profile_id, Infra::Types::UUID + attribute :tier, Infra::Types::String + + alias_method :aggregate_id, :profile_id + end + + class CompleteTrack < Infra::Command + attribute :profile_id, Infra::Types::UUID + attribute :track, Infra::Types::String + + alias_method :aggregate_id, :profile_id + end + + class MaintainStreak < Infra::Command + attribute :profile_id, Infra::Types::UUID + attribute :streak, Infra::Types::Integer + + alias_method :aggregate_id, :profile_id + end + + class EarnBadge < Infra::Command + attribute :profile_id, Infra::Types::UUID + attribute :badge, Infra::Types::String + + alias_method :aggregate_id, :profile_id + end + + class UpdateLeaderboard < Infra::Command + attribute :leaderboard_id, Infra::Types::UUID + attribute :profile_id, Infra::Types::UUID + attribute :score, Infra::Types::Integer + + alias_method :aggregate_id, :leaderboard_id + end +end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/events.rb b/apps/govquests-api/govquests/gamification/lib/gamification/events.rb new file mode 100644 index 0000000..7f5c214 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/lib/gamification/events.rb @@ -0,0 +1,33 @@ +# govquests/gamification/lib/gamification/events.rb +module Gamification + class ScoreUpdated < Infra::Event + attribute :profile_id, Infra::Types::UUID + attribute :points, Infra::Types::Integer + end + + class TierAchieved < Infra::Event + attribute :profile_id, Infra::Types::UUID + attribute :tier, Infra::Types::String + end + + class TrackCompleted < Infra::Event + attribute :profile_id, Infra::Types::UUID + attribute :track, Infra::Types::String + end + + class StreakMaintained < Infra::Event + attribute :profile_id, Infra::Types::UUID + attribute :streak, Infra::Types::Integer + end + + class BadgeEarned < Infra::Event + attribute :profile_id, Infra::Types::UUID + attribute :badge, Infra::Types::String + end + + class LeaderboardUpdated < Infra::Event + attribute :leaderboard_id, Infra::Types::UUID + attribute :profile_id, Infra::Types::UUID + attribute :score, Infra::Types::Integer + end +end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/game_profile.rb b/apps/govquests-api/govquests/gamification/lib/gamification/game_profile.rb new file mode 100644 index 0000000..0ed2df3 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/lib/gamification/game_profile.rb @@ -0,0 +1,72 @@ +# govquests/gamification/lib/gamification/game_profile.rb +module Gamification + class GameProfile + include AggregateRoot + + def initialize(id) + @id = id + @score = 0 + @tier = nil + @track = nil + @streak = 0 + @badges = [] + end + + def update_score(points) + apply ScoreUpdated.new(data: { + profile_id: @id, + points: points + }) + end + + def achieve_tier(tier) + apply TierAchieved.new(data: { + profile_id: @id, + tier: tier + }) + end + + def complete_track(track) + apply TrackCompleted.new(data: { + profile_id: @id, + track: track + }) + end + + def maintain_streak(streak) + apply StreakMaintained.new(data: { + profile_id: @id, + streak: streak + }) + end + + def earn_badge(badge) + apply BadgeEarned.new(data: { + profile_id: @id, + badge: badge + }) + end + + private + + on ScoreUpdated do |event| + @score += event.data[:points] + end + + on TierAchieved do |event| + @tier = event.data[:tier] + end + + on TrackCompleted do |event| + @track = event.data[:track] + end + + on StreakMaintained do |event| + @streak = event.data[:streak] + end + + on BadgeEarned do |event| + @badges << event.data[:badge] + end + end +end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/leaderboard.rb b/apps/govquests-api/govquests/gamification/lib/gamification/leaderboard.rb new file mode 100644 index 0000000..fbe8776 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/lib/gamification/leaderboard.rb @@ -0,0 +1,25 @@ +# govquests/gamification/lib/gamification/leaderboard.rb +module Gamification + class Leaderboard + include AggregateRoot + + def initialize(id) + @id = id + @entries = {} + end + + def update_score(profile_id, score) + apply LeaderboardUpdated.new(data: { + leaderboard_id: @id, + profile_id: profile_id, + score: score + }) + end + + private + + on LeaderboardUpdated do |event| + @entries[event.data[:profile_id]] = event.data[:score] + end + end +end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/on_game_profile_commands.rb b/apps/govquests-api/govquests/gamification/lib/gamification/on_game_profile_commands.rb new file mode 100644 index 0000000..a0afb48 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/lib/gamification/on_game_profile_commands.rb @@ -0,0 +1,31 @@ +module Gamification + class OnGameProfileCommands + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + case command + when UpdateLeaderboard + @repository.with_aggregate(Leaderboard, command.aggregate_id) do |leaderboard| + leaderboard.update_score(command.profile_id, command.score) + end + else + @repository.with_aggregate(GameProfile, command.aggregate_id) do |profile| + case command + when UpdateScore + profile.update_score(command.points) + when AchieveTier + profile.achieve_tier(command.tier) + when CompleteTrack + profile.complete_track(command.track) + when MaintainStreak + profile.maintain_streak(command.streak) + when EarnBadge + profile.earn_badge(command.badge) + end + end + end + end + end +end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/value_objects.rb b/apps/govquests-api/govquests/gamification/lib/gamification/value_objects.rb new file mode 100644 index 0000000..2b0043a --- /dev/null +++ b/apps/govquests-api/govquests/gamification/lib/gamification/value_objects.rb @@ -0,0 +1,9 @@ +module Gamification + class RewardType < Dry::Struct + values :points, :experience, :badge, :token + end + + class RewardDeliveryStatus < Dry::Struct + values :pending, :issued, :claimed, :expired + end +end diff --git a/apps/govquests-api/govquests/gamification/test/game_profile_test.rb b/apps/govquests-api/govquests/gamification/test/game_profile_test.rb new file mode 100644 index 0000000..f6ac261 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/test/game_profile_test.rb @@ -0,0 +1,39 @@ +require_relative "test_helper" + +module Gamification + class GameProfileTest < Test + cover "Gamification::GameProfile" + + def setup + super + @profile_id = SecureRandom.uuid + @profile = GameProfile.new(@profile_id) + end + + def test_update_score + points = 100 + + @profile.update_score(points) + + events = @profile.unpublished_events.to_a + assert_equal 1, events.size + event = events.first + assert_instance_of ScoreUpdated, event + assert_equal @profile_id, event.data[:profile_id] + assert_equal points, event.data[:points] + end + + def test_achieve_tier + tier = "Gold" + + @profile.achieve_tier(tier) + + events = @profile.unpublished_events.to_a + assert_equal 1, events.size + event = events.first + assert_instance_of TierAchieved, event + assert_equal @profile_id, event.data[:profile_id] + assert_equal tier, event.data[:tier] + end + end +end diff --git a/apps/govquests-api/govquests/gamification/test/test_helper.rb b/apps/govquests-api/govquests/gamification/test/test_helper.rb new file mode 100644 index 0000000..d4b14e0 --- /dev/null +++ b/apps/govquests-api/govquests/gamification/test/test_helper.rb @@ -0,0 +1,19 @@ +require "minitest/autorun" +require "mutant/minitest/coverage" + +require_relative "../lib/gamification" + +module Questing + class Test < Infra::InMemoryTest + def before_setup + super + Configuration.new.call(event_store, command_bus) + end + + private + + def fake_login + "fake_login" + end + end +end diff --git a/apps/govquests-api/govquests/notifications/.mutant.yml b/apps/govquests-api/govquests/notifications/.mutant.yml new file mode 100644 index 0000000..a4ea1c5 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/.mutant.yml @@ -0,0 +1,11 @@ +requires: + - ./test/test_helper +integration: minitest +usage: opensource +coverage_criteria: + process_abort: true +matcher: + subjects: + - Questing* + ignore: + - Questing::Configuration#call diff --git a/apps/govquests-api/govquests/notifications/Gemfile b/apps/govquests-api/govquests/notifications/Gemfile new file mode 100644 index 0000000..00fef6d --- /dev/null +++ b/apps/govquests-api/govquests/notifications/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +eval_gemfile "../../infra/Gemfile.test" +gem "infra", path: "../../infra" diff --git a/apps/govquests-api/govquests/notifications/Gemfile.lock b/apps/govquests-api/govquests/notifications/Gemfile.lock new file mode 100644 index 0000000..d3c8cdd --- /dev/null +++ b/apps/govquests-api/govquests/notifications/Gemfile.lock @@ -0,0 +1,110 @@ +PATH + remote: ../../infra + specs: + infra (1.0.0) + aggregate_root (~> 2.15) + arkency-command_bus + dry-struct + dry-types + rake + ruby_event_store (~> 2.15) + ruby_event_store-transformations + +GEM + remote: https://oss:7AXfeZdAfCqL1PvHm2nvDJO6Zd9UW8IK@gem.mutant.dev/ + specs: + mutant-license (0.1.1.2.1627430819213747598431630701693729869473.6) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + aggregate_root (2.15.0) + ruby_event_store (= 2.15.0) + arkency-command_bus (0.4.1) + concurrent-ruby + ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.4) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + diff-lcs (1.5.1) + drb (2.2.0) + ruby2_keywords + dry-core (1.0.1) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.7.1) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + minitest (5.25.0) + mutant (0.12.4) + diff-lcs (~> 1.3) + parser (~> 3.3.0) + regexp_parser (~> 2.9.0) + sorbet-runtime (~> 0.5.0) + unparser (~> 0.6.14) + mutant-minitest (0.12.4) + minitest (~> 5.11) + mutant (= 0.12.4) + mutex_m (0.2.0) + parser (3.3.4.2) + ast (~> 2.4.1) + racc + racc (1.8.1) + rake (13.1.0) + regexp_parser (2.9.2) + ruby2_keywords (0.0.5) + ruby_event_store (2.15.0) + concurrent-ruby (~> 1.0, >= 1.1.6) + ruby_event_store-transformations (0.1.0) + activesupport (>= 5.0) + ruby_event_store (>= 2.0.0, < 3.0.0) + sorbet-runtime (0.5.11525) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unparser (0.6.15) + diff-lcs (~> 1.3) + parser (>= 3.3.0) + zeitwerk (2.6.12) + +PLATFORMS + arm64-darwin-20 + arm64-darwin-21 + ruby + x86_64-darwin-20 + x86_64-linux + +DEPENDENCIES + infra! + minitest (= 5.25.0)! + mutant-license! + mutant-minitest (= 0.12.4)! + +BUNDLED WITH + 2.5.9 diff --git a/apps/govquests-api/govquests/notifications/Makefile b/apps/govquests-api/govquests/notifications/Makefile new file mode 100644 index 0000000..23850fa --- /dev/null +++ b/apps/govquests-api/govquests/notifications/Makefile @@ -0,0 +1,10 @@ +install: + @bundle install + +test: + @bundle exec ruby -e "require \"rake/rake_test_loader\"" test/*_test.rb + +mutate: + @RAILS_ENV=test bundle exec mutant run + +.PHONY: install test mutate diff --git a/apps/govquests-api/govquests/notifications/README.md b/apps/govquests-api/govquests/notifications/README.md new file mode 100644 index 0000000..b187021 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/README.md @@ -0,0 +1,7 @@ +# Questing + +#### Up and running + +``` +make install test mutate +``` diff --git a/apps/govquests-api/govquests/notifications/lib/notifications.rb b/apps/govquests-api/govquests/notifications/lib/notifications.rb new file mode 100644 index 0000000..59284b6 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications.rb @@ -0,0 +1,18 @@ +require "infra" +require_relative "notifications/commands" +require_relative "notifications/events" +require_relative "notifications/template_commands" +require_relative "notifications/template_events" +require_relative "notifications/on_notification_commands" +require_relative "notifications/notification" + +module Notifications + class Configuration + def call(event_store, command_bus) + command_handler = OnNotificationCommands.new(event_store) + command_bus.register(CreateNotification, command_handler) + command_bus.register(SendNotification, command_handler) + command_bus.register(MarkNotificationAsRead, command_handler) + end + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/commands.rb b/apps/govquests-api/govquests/notifications/lib/notifications/commands.rb new file mode 100644 index 0000000..88be444 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/commands.rb @@ -0,0 +1,22 @@ +module Notifications + class CreateNotification < Infra::Command + attribute :notification_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + attribute :content, Infra::Types::String + attribute :notification_type, Infra::Types::String + + alias_method :aggregate_id, :notification_id + end + + class SendNotification < Infra::Command + attribute :notification_id, Infra::Types::UUID + + alias_method :aggregate_id, :notification_id + end + + class MarkNotificationAsRead < Infra::Command + attribute :notification_id, Infra::Types::UUID + + alias_method :aggregate_id, :notification_id + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/events.rb b/apps/govquests-api/govquests/notifications/lib/notifications/events.rb new file mode 100644 index 0000000..ea5492f --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/events.rb @@ -0,0 +1,33 @@ +module Notifications + class NotificationCreated < Infra::Event + attribute :notification_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + attribute :content, Infra::Types::String + attribute :notification_type, Infra::Types::String + end + + class NotificationScheduled < Infra::Event + attribute :notification_id, Infra::Types::UUID + attribute :scheduled_time, Infra::Types::Time + end + + class NotificationSent < Infra::Event + attribute :notification_id, Infra::Types::UUID + attribute :sent_at, Infra::Types::Time + end + + class NotificationReceived < Infra::Event + attribute :notification_id, Infra::Types::UUID + attribute :received_at, Infra::Types::Time + end + + class NotificationOpened < Infra::Event + attribute :notification_id, Infra::Types::UUID + attribute :opened_at, Infra::Types::Time + end + + class NotificationMarkedAsRead < Infra::Event + attribute :notification_id, Infra::Types::UUID + attribute :read_at, Infra::Types::Time + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/notification.rb b/apps/govquests-api/govquests/notifications/lib/notifications/notification.rb new file mode 100644 index 0000000..319b48b --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/notification.rb @@ -0,0 +1,53 @@ +module Notifications + class Notification + include AggregateRoot + + def initialize(id) + @id = id + @user_id = nil + @content = nil + @type = nil + @sent_at = nil + @read_at = nil + end + + def create(user_id, content, type) + apply NotificationCreated.new(data: { + notification_id: @id, + user_id: user_id, + content: content, + notification_type: type + }) + end + + def send_notification + apply NotificationSent.new(data: { + notification_id: @id, + sent_at: Time.now + }) + end + + def mark_as_read + apply NotificationMarkedAsRead.new(data: { + notification_id: @id, + read_at: Time.now + }) + end + + private + + on NotificationCreated do |event| + @user_id = event.data[:user_id] + @content = event.data[:content] + @type = event.data[:notification_type] + end + + on NotificationSent do |event| + @sent_at = event.data[:sent_at] + end + + on NotificationMarkedAsRead do |event| + @read_at = event.data[:read_at] + end + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/notification_template.rb b/apps/govquests-api/govquests/notifications/lib/notifications/notification_template.rb new file mode 100644 index 0000000..5328827 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/notification_template.rb @@ -0,0 +1,54 @@ +module Notifications + class NotificationTemplate + include AggregateRoot + + def initialize(id) + @id = id + @name = nil + @content = nil + @type = nil + end + + def create(name, content, type) + apply NotificationTemplateCreated.new(data: { + template_id: @id, + name: name, + content: content, + type: type + }) + end + + def update(name: nil, content: nil, type: nil) + apply NotificationTemplateUpdated.new(data: { + template_id: @id, + name: name, + content: content, + type: type + }) + end + + def delete + apply NotificationTemplateDeleted.new(data: { + template_id: @id + }) + end + + private + + on NotificationTemplateCreated do |event| + @name = event.data[:name] + @content = event.data[:content] + @type = event.data[:type] + end + + on NotificationTemplateUpdated do |event| + @name = event.data[:name] if event.data[:name] + @content = event.data[:content] if event.data[:content] + @type = event.data[:type] if event.data[:type] + end + + on NotificationTemplateDeleted do |_event| + @deleted = true + end + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/on_notification_commands.rb b/apps/govquests-api/govquests/notifications/lib/notifications/on_notification_commands.rb new file mode 100644 index 0000000..43f9d89 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/on_notification_commands.rb @@ -0,0 +1,20 @@ +module Notifications + class OnNotificationCommands + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + @repository.with_aggregate(Notification, command.aggregate_id) do |notification| + case command + when CreateNotification + notification.create(command.user_id, command.content, command.type) + when SendNotification + notification.send_notification + when MarkNotificationAsRead + notification.mark_as_read + end + end + end + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/on_template_commands.rb b/apps/govquests-api/govquests/notifications/lib/notifications/on_template_commands.rb new file mode 100644 index 0000000..d00d9ba --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/on_template_commands.rb @@ -0,0 +1,24 @@ +# govquests/notifications/lib/notifications/on_template_commands.rb + +module Notifications + class OnTemplateCommands + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + @repository.with_aggregate(NotificationTemplate, command.aggregate_id) do |template| + case command + when CreateNotificationTemplate + template.create(command.name, command.content, command.type) + when UpdateNotificationTemplate + template.update(name: command.name, content: command.content, type: command.type) + when DeleteNotificationTemplate + template.delete + else + raise "Unknown command: #{command.class}" + end + end + end + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/template_commands.rb b/apps/govquests-api/govquests/notifications/lib/notifications/template_commands.rb new file mode 100644 index 0000000..b724135 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/template_commands.rb @@ -0,0 +1,25 @@ +module Notifications + class CreateNotificationTemplate < Infra::Command + attribute :template_id, Infra::Types::UUID + attribute :name, Infra::Types::String + attribute :content, Infra::Types::String + attribute :template_type, Infra::Types::String + + alias_method :aggregate_id, :template_id + end + + class UpdateNotificationTemplate < Infra::Command + attribute :template_id, Infra::Types::UUID + attribute :name, Infra::Types::String.optional + attribute :content, Infra::Types::String.optional + attribute :template_type, Infra::Types::String.optional + + alias_method :aggregate_id, :template_id + end + + class DeleteNotificationTemplate < Infra::Command + attribute :template_id, Infra::Types::UUID + + alias_method :aggregate_id, :template_id + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/template_events.rb b/apps/govquests-api/govquests/notifications/lib/notifications/template_events.rb new file mode 100644 index 0000000..8cfa944 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/template_events.rb @@ -0,0 +1,19 @@ +module Notifications + class NotificationTemplateCreated < Infra::Event + attribute :template_id, Infra::Types::UUID + attribute :name, Infra::Types::String + attribute :content, Infra::Types::String + attribute :template_type, Infra::Types::String + end + + class NotificationTemplateUpdated < Infra::Event + attribute :template_id, Infra::Types::UUID + attribute :name, Infra::Types::String.optional + attribute :content, Infra::Types::String.optional + attribute :template_type, Infra::Types::String.optional + end + + class NotificationTemplateDeleted < Infra::Event + attribute :template_id, Infra::Types::UUID + end +end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/value_objects.rb b/apps/govquests-api/govquests/notifications/lib/notifications/value_objects.rb new file mode 100644 index 0000000..208d5de --- /dev/null +++ b/apps/govquests-api/govquests/notifications/lib/notifications/value_objects.rb @@ -0,0 +1,71 @@ +module Notifications + class NotificationStatus < Dry::Struct + attribute :status, Infra::Types::String + + def self.created + new(status: "created") + end + + def self.scheduled + new(status: "scheduled") + end + + def self.sent + new(status: "sent") + end + + def self.received + new(status: "received") + end + + def self.opened + new(status: "opened") + end + end + + class NotificationType < Dry::Struct + attribute :notification_type, Infra::Types::String + end + + class NotificationContent < Dry::Struct + attribute :content, Infra::Types::String + + def initialize(content) + super + raise "Content cannot be empty" if content.strip.empty? + end + end + + class NotificationChannel < Dry::Struct + attribute :channel, Infra::Types::String + + VALID_CHANNELS = ["email", "SMS", "push"] + + def initialize(channel) + super + raise "Invalid channel" unless VALID_CHANNELS.include?(channel) + end + end + + class NotificationPriority < Dry::Struct + attribute :priority, Infra::Types::Integer.constrained(gt: 0, lt: 6) # 1 (low) to 5 (high) + end + + class TemplateName < Dry::Struct + attribute :name, Infra::Types::String + + def initialize(name) + super + raise "Template name cannot be empty" if name.strip.empty? + end + end + + class TemplateContent < Dry::Struct + attribute :content, Infra::Types::String + + def initialize(content) + super + raise "Template content cannot be empty" if content.strip.empty? + end + end +end diff --git a/apps/govquests-api/govquests/notifications/test/notification_test.rb b/apps/govquests-api/govquests/notifications/test/notification_test.rb new file mode 100644 index 0000000..0fcc1ce --- /dev/null +++ b/apps/govquests-api/govquests/notifications/test/notification_test.rb @@ -0,0 +1,57 @@ +require_relative "test_helper" + +module Notifications + class NotificationTest < Test + cover "Notifications::Notification" + + def setup + super + @notification_id = SecureRandom.uuid + @notification = Notification.new(@notification_id) + end + + def test_create_a_new_notification + user_id = SecureRandom.uuid + content = "You've earned a badge!" + type = "tier_achieved" + + @notification.create(user_id, content, type) + + events = @notification.unpublished_events.to_a + assert_equal 1, events.size + event = events.first + assert_instance_of NotificationCreated, event + assert_equal @notification_id, event.data[:notification_id] + assert_equal user_id, event.data[:user_id] + assert_equal content, event.data[:content] + assert_equal type, event.data[:notification_type] + end + + def test_send_a_notification + @notification.create(SecureRandom.uuid, "Test content", "test") + + @notification.send_notification + + events = @notification.unpublished_events.to_a + assert_equal 2, events.size + event = events.last + assert_instance_of NotificationSent, event + assert_equal @notification_id, event.data[:notification_id] + assert_instance_of Time, event.data[:sent_at] + end + + def test_mark_notification_as_read + @notification.create(SecureRandom.uuid, "Test content", "test") + @notification.send_notification + + @notification.mark_as_read + + events = @notification.unpublished_events.to_a + assert_equal 3, events.size + event = events.last + assert_instance_of NotificationMarkedAsRead, event + assert_equal @notification_id, event.data[:notification_id] + assert_instance_of Time, event.data[:read_at] + end + end +end diff --git a/apps/govquests-api/govquests/notifications/test/test_helper.rb b/apps/govquests-api/govquests/notifications/test/test_helper.rb new file mode 100644 index 0000000..f824b17 --- /dev/null +++ b/apps/govquests-api/govquests/notifications/test/test_helper.rb @@ -0,0 +1,19 @@ +require "minitest/autorun" +require "mutant/minitest/coverage" + +require_relative "../lib/notifications" + +module Questing + class Test < Infra::InMemoryTest + def before_setup + super + Configuration.new.call(event_store, command_bus) + end + + private + + def fake_login + "fake_login" + end + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing/events.rb b/apps/govquests-api/govquests/questing/lib/questing/events.rb index ba98e71..87b6a2d 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/events.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/events.rb @@ -10,6 +10,11 @@ class QuestCreated < Infra::Event attribute :subquests, Infra::Types::Array.optional end + class UserStartedQuest < Infra::Event + attribute :quest_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + end + class QuestRequirementAdded < Infra::Event attribute :quest_id, Infra::Types::UUID attribute :requirement, Infra::Types::Hash diff --git a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb index 76f7634..06d31de 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb @@ -1,4 +1,3 @@ -# govquests/questing/lib/questing/on_quest_commands.rb module Questing class OnQuestCommands def initialize(event_store) @@ -11,7 +10,7 @@ def call(command) when CreateQuest quest.create( command.audience, - command.type, + command.quest_type, command.duration, command.difficulty, command.requirements, diff --git a/apps/govquests-api/govquests/rewarding/.mutant.yml b/apps/govquests-api/govquests/rewarding/.mutant.yml new file mode 100644 index 0000000..a4ea1c5 --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/.mutant.yml @@ -0,0 +1,11 @@ +requires: + - ./test/test_helper +integration: minitest +usage: opensource +coverage_criteria: + process_abort: true +matcher: + subjects: + - Questing* + ignore: + - Questing::Configuration#call diff --git a/apps/govquests-api/govquests/rewarding/Gemfile b/apps/govquests-api/govquests/rewarding/Gemfile new file mode 100644 index 0000000..00fef6d --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +eval_gemfile "../../infra/Gemfile.test" +gem "infra", path: "../../infra" diff --git a/apps/govquests-api/govquests/rewarding/Gemfile.lock b/apps/govquests-api/govquests/rewarding/Gemfile.lock new file mode 100644 index 0000000..d3c8cdd --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/Gemfile.lock @@ -0,0 +1,110 @@ +PATH + remote: ../../infra + specs: + infra (1.0.0) + aggregate_root (~> 2.15) + arkency-command_bus + dry-struct + dry-types + rake + ruby_event_store (~> 2.15) + ruby_event_store-transformations + +GEM + remote: https://oss:7AXfeZdAfCqL1PvHm2nvDJO6Zd9UW8IK@gem.mutant.dev/ + specs: + mutant-license (0.1.1.2.1627430819213747598431630701693729869473.6) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + aggregate_root (2.15.0) + ruby_event_store (= 2.15.0) + arkency-command_bus (0.4.1) + concurrent-ruby + ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.4) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + diff-lcs (1.5.1) + drb (2.2.0) + ruby2_keywords + dry-core (1.0.1) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.7.1) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + minitest (5.25.0) + mutant (0.12.4) + diff-lcs (~> 1.3) + parser (~> 3.3.0) + regexp_parser (~> 2.9.0) + sorbet-runtime (~> 0.5.0) + unparser (~> 0.6.14) + mutant-minitest (0.12.4) + minitest (~> 5.11) + mutant (= 0.12.4) + mutex_m (0.2.0) + parser (3.3.4.2) + ast (~> 2.4.1) + racc + racc (1.8.1) + rake (13.1.0) + regexp_parser (2.9.2) + ruby2_keywords (0.0.5) + ruby_event_store (2.15.0) + concurrent-ruby (~> 1.0, >= 1.1.6) + ruby_event_store-transformations (0.1.0) + activesupport (>= 5.0) + ruby_event_store (>= 2.0.0, < 3.0.0) + sorbet-runtime (0.5.11525) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unparser (0.6.15) + diff-lcs (~> 1.3) + parser (>= 3.3.0) + zeitwerk (2.6.12) + +PLATFORMS + arm64-darwin-20 + arm64-darwin-21 + ruby + x86_64-darwin-20 + x86_64-linux + +DEPENDENCIES + infra! + minitest (= 5.25.0)! + mutant-license! + mutant-minitest (= 0.12.4)! + +BUNDLED WITH + 2.5.9 diff --git a/apps/govquests-api/govquests/rewarding/Makefile b/apps/govquests-api/govquests/rewarding/Makefile new file mode 100644 index 0000000..23850fa --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/Makefile @@ -0,0 +1,10 @@ +install: + @bundle install + +test: + @bundle exec ruby -e "require \"rake/rake_test_loader\"" test/*_test.rb + +mutate: + @RAILS_ENV=test bundle exec mutant run + +.PHONY: install test mutate diff --git a/apps/govquests-api/govquests/rewarding/README.md b/apps/govquests-api/govquests/rewarding/README.md new file mode 100644 index 0000000..b187021 --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/README.md @@ -0,0 +1,7 @@ +# Questing + +#### Up and running + +``` +make install test mutate +``` diff --git a/apps/govquests-api/govquests/rewarding/lib/rewarding.rb b/apps/govquests-api/govquests/rewarding/lib/rewarding.rb new file mode 100644 index 0000000..24e917a --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/lib/rewarding.rb @@ -0,0 +1,16 @@ +require "infra" +require_relative "rewarding/commands" +require_relative "rewarding/events" +require_relative "rewarding/on_reward_commands" +require_relative "rewarding/reward" + +module Rewarding + class Configuration + def call(event_store, command_bus) + command_handler = OnRewardCommands.new(event_store) + command_bus.register(CreateReward, command_handler) + command_bus.register(IssueReward, command_handler) + command_bus.register(ClaimReward, command_handler) + end + end +end diff --git a/apps/govquests-api/govquests/rewarding/lib/rewarding/commands.rb b/apps/govquests-api/govquests/rewarding/lib/rewarding/commands.rb new file mode 100644 index 0000000..8e3e0c3 --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/lib/rewarding/commands.rb @@ -0,0 +1,24 @@ +module Rewarding + class CreateReward < Infra::Command + attribute :reward_id, Infra::Types::UUID + attribute :reward_type, Infra::Types::String + attribute :value, Infra::Types::Integer + attribute :expiry_date, Infra::Types::DateTime.optional + + alias_method :aggregate_id, :reward_id + end + + class IssueReward < Infra::Command + attribute :reward_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + + alias_method :aggregate_id, :reward_id + end + + class ClaimReward < Infra::Command + attribute :reward_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + + alias_method :aggregate_id, :reward_id + end +end diff --git a/apps/govquests-api/govquests/rewarding/lib/rewarding/events.rb b/apps/govquests-api/govquests/rewarding/lib/rewarding/events.rb new file mode 100644 index 0000000..d9d7776 --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/lib/rewarding/events.rb @@ -0,0 +1,26 @@ +module Rewarding + class RewardCreated < Infra::Event + attribute :reward_id, Infra::Types::UUID + attribute :reward_type, Infra::Types::String + attribute :value, Infra::Types::Integer + attribute :expiry_date, Infra::Types::Time.optional + end + + class RewardIssued < Infra::Event + attribute :reward_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + end + + class RewardClaimed < Infra::Event + attribute :reward_id, Infra::Types::UUID + attribute :user_id, Infra::Types::UUID + end + + class RewardExpired < Infra::Event + attribute :reward_id, Infra::Types::UUID + end + + class RewardInventoryDepleted < Infra::Event + attribute :reward_id, Infra::Types::UUID + end +end diff --git a/apps/govquests-api/govquests/rewarding/lib/rewarding/on_reward_commands.rb b/apps/govquests-api/govquests/rewarding/lib/rewarding/on_reward_commands.rb new file mode 100644 index 0000000..c4b78f8 --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/lib/rewarding/on_reward_commands.rb @@ -0,0 +1,20 @@ +module Rewarding + class OnRewardCommands + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + @repository.with_aggregate(Reward, command.aggregate_id) do |reward| + case command + when CreateReward + reward.create(command.reward_type, command.value, command.expiry_date) + when IssueReward + reward.issue(command.user_id) + when ClaimReward + reward.claim(command.user_id) + end + end + end + end +end diff --git a/apps/govquests-api/govquests/rewarding/lib/rewarding/reward.rb b/apps/govquests-api/govquests/rewarding/lib/rewarding/reward.rb new file mode 100644 index 0000000..05f0ab9 --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/lib/rewarding/reward.rb @@ -0,0 +1,50 @@ +module Rewarding + class Reward + include AggregateRoot + + AlreadyClaimed = Class.new(StandardError) + + def initialize(id) + @id = id + @reward_type = nil + @value = nil + @expiry_date = nil + @issued_to = nil + @claimed = false + end + + def create(reward_type, value, expiry_date = nil) + apply RewardCreated.new(data: { + reward_id: @id, + reward_type: reward_type, + value: value, + expiry_date: expiry_date + }) + end + + def issue(user_id) + apply RewardIssued.new(data: {reward_id: @id, user_id: user_id}) + end + + def claim(user_id) + raise AlreadyClaimed if @claimed + apply RewardClaimed.new(data: {reward_id: @id, user_id: user_id}) + end + + private + + on RewardCreated do |event| + @reward_type = event.data[:reward_type] + @value = event.data[:value] + @expiry_date = event.data[:expiry_date] + end + + on RewardIssued do |event| + @issued_to = event.data[:user_id] + end + + on RewardClaimed do |event| + @claimed = true + end + end +end diff --git a/apps/govquests-api/govquests/rewarding/lib/rewarding/value_objects.rb b/apps/govquests-api/govquests/rewarding/lib/rewarding/value_objects.rb new file mode 100644 index 0000000..74ebd41 --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/lib/rewarding/value_objects.rb @@ -0,0 +1,21 @@ +module Rewarding + class RewardType < Dry::Struct + values :Experience, :Attribute, :Points + end + + class RewardValue < Dry::Struct + attribute :amount, Infra::Types::Integer + end + + class RewardExpiryDate < Dry::Struct + attribute :expiry_date, Infra::Types::Time + end + + class Amount < Dry::Struct + attribute :value, Infra::Types::Integer + end + + class RewardDeliveryStatus < Dry::Struct + values :Pending, :Issued, :Claimed, :Expired + end +end diff --git a/apps/govquests-api/govquests/rewarding/test/reward_test.rb b/apps/govquests-api/govquests/rewarding/test/reward_test.rb new file mode 100644 index 0000000..9ec0c3f --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/test/reward_test.rb @@ -0,0 +1,69 @@ +require_relative "test_helper" + +module Rewarding + class RewardTest < Test + cover "Rewarding::Reward" + + def setup + super + @reward_id = SecureRandom.uuid + @reward = Reward.new(@reward_id) + end + + def test_create_a_new_reward + reward_type = "points" + value = 100 + expiry_date = Time.now + 30.days + + @reward.create(reward_type, value, expiry_date) + + events = @reward.unpublished_events.to_a + assert_equal 1, events.size + event = events.first + assert_instance_of RewardCreated, event + assert_equal @reward_id, event.data[:reward_id] + assert_equal reward_type, event.data[:reward_type] + assert_equal value, event.data[:value] + assert_equal expiry_date, event.data[:expiry_date] + end + + def test_issue_a_reward + @reward.create("points", 100) + user_id = SecureRandom.uuid + + @reward.issue(user_id) + + events = @reward.unpublished_events.to_a + assert_equal 2, events.size + event = events.last + assert_instance_of RewardIssued, event + assert_equal @reward_id, event.data[:reward_id] + assert_equal user_id, event.data[:user_id] + end + + def test_claim_a_reward + @reward.create("points", 100) + @reward.issue(SecureRandom.uuid) + user_id = SecureRandom.uuid + + @reward.claim(user_id) + + events = @reward.unpublished_events.to_a + assert_equal 3, events.size + event = events.last + assert_instance_of RewardClaimed, event + assert_equal @reward_id, event.data[:reward_id] + assert_equal user_id, event.data[:user_id] + end + + def test_cannot_claim_an_already_claimed_reward + @reward.create("points", 100) + @reward.issue(SecureRandom.uuid) + @reward.claim(SecureRandom.uuid) + + assert_raises(Reward::AlreadyClaimed) do + @reward.claim(SecureRandom.uuid) + end + end + end +end diff --git a/apps/govquests-api/govquests/rewarding/test/test_helper.rb b/apps/govquests-api/govquests/rewarding/test/test_helper.rb new file mode 100644 index 0000000..ea90990 --- /dev/null +++ b/apps/govquests-api/govquests/rewarding/test/test_helper.rb @@ -0,0 +1,13 @@ +require "minitest/autorun" +require "mutant/minitest/coverage" + +require_relative "../lib/rewarding" + +module Questing + class Test < Infra::InMemoryTest + def before_setup + super + Configuration.new.call(event_store, command_bus) + end + end +end diff --git a/apps/govquests-api/rails_app/.kamal/secrets b/apps/govquests-api/rails_app/.kamal/secrets index 9a771a3..bc720a2 100644 --- a/apps/govquests-api/rails_app/.kamal/secrets +++ b/apps/govquests-api/rails_app/.kamal/secrets @@ -3,7 +3,7 @@ # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. # Example of extracting secrets from 1password (or another compatible pw manager) -# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# SECRETS=$(kamal secrets fetch --adapter 1password --user your-user --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb new file mode 100644 index 0000000..bdde1b9 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb @@ -0,0 +1,12 @@ +module ActionTracking + class OnActionCreated + def call(event) + ActionReadModel.find_or_create_by(action_id: event.data[:action_id]).update( + content: event.data[:content], + priority: event.data[:priority], + channel: event.data[:channel], + status: "Created" + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb new file mode 100644 index 0000000..9bb25fc --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb @@ -0,0 +1,19 @@ +module ActionTracking + class OnActionExecuted + def call(event) + action_log_id = SecureRandom.uuid + action_id = event.data[:action_id] + user_id = event.data[:user_id] + timestamp = event.data[:timestamp] + status = "Executed" + + ActionLogReadModel.create!( + action_log_id: action_log_id, + action_id: action_id, + user_id: user_id, + executed_at: timestamp, + status: status + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb new file mode 100644 index 0000000..9862f2f --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb @@ -0,0 +1,29 @@ +# rails_app/app/read_models/action_tracking/read_model_configuration.rb +module ActionTracking + class ActionReadModel < ApplicationRecord + self.table_name = "actions" + + validates :action_id, presence: true, uniqueness: true + validates :content, presence: true + validates :priority, presence: true + validates :channel, presence: true + validates :status, presence: true + end + + class ActionLogReadModel < ApplicationRecord + self.table_name = "action_logs" + + validates :action_log_id, presence: true, uniqueness: true + validates :action_id, presence: true + validates :user_id, presence: true + validates :executed_at, presence: true + validates :status, presence: true + end + + class ReadModelConfiguration + def call(event_store) + event_store.subscribe(OnActionCreated, to: [ ActionTracking::ActionCreated ]) + event_store.subscribe(OnActionExecuted, to: [ ActionTracking::ActionExecuted ]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb index f5f9444..cefa1e7 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb @@ -1,4 +1,3 @@ -# app/read_models/client_authentication/create_user.rb module Authentication class OnUserRegistered def call(event) @@ -8,11 +7,11 @@ def call(event) wallet_address = event.data.fetch(:wallet_address) chain_id = event.data.fetch(:chain_id) - UserReadModel.find_or_create_by(user_id: user_id).update( - email: email, - user_type: user_type, - wallets: [ { wallet_address: wallet_address, chain_id: chain_id } ] - ) + user = UserReadModel.find_or_initialize_by(user_id: user_id) + user.email = email + user.user_type = user_type + user.wallets = [ { wallet_address: wallet_address, chain_id: chain_id } ] + user.save! end end end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb b/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb new file mode 100644 index 0000000..cf8b2da --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb @@ -0,0 +1,21 @@ +module Gamification + class OnBadgeEarned + def call(event) + profile_id = event.data[:profile_id] + badge = event.data[:badge] + + game_profile = GameProfileReadModel.find_by(profile_id: profile_id) + if game_profile + badges = game_profile.badges || [] + if badges.include?(badge) + Rails.logger.info "Badge '#{badge}' already exists for GameProfile #{profile_id}" + else + game_profile.update(badges: badges + [ badge ]) + Rails.logger.info "Added badge '#{badge}' to GameProfile #{profile_id}" + end + else + Rails.logger.warn "GameProfile #{profile_id} not found for BadgeEarned event" + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/on_leaderboard_updated.rb b/apps/govquests-api/rails_app/app/read_models/gamification/on_leaderboard_updated.rb new file mode 100644 index 0000000..911aaf4 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/gamification/on_leaderboard_updated.rb @@ -0,0 +1,28 @@ +module Gamification + class OnLeaderboardUpdated + def call(event) + leaderboard = LeaderboardReadModel.find_by(leaderboard_id: event.data[:leaderboard_id]) + return unless leaderboard + + entry = LeaderboardEntryReadModel.find_or_initialize_by( + leaderboard_id: event.data[:leaderboard_id], + profile_id: event.data[:profile_id] + ) + + entry.leaderboard = leaderboard + entry.score = event.data[:score] + entry.rank = calculate_rank(leaderboard.leaderboard_id, entry.score) + + entry.save! + end + + private + + def calculate_rank(leaderboard_id, score) + higher_scores_count = LeaderboardEntryReadModel.where(leaderboard_id: leaderboard_id) + .where("score > ?", score) + .count + higher_scores_count + 1 + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/on_streak_maintained.rb b/apps/govquests-api/rails_app/app/read_models/gamification/on_streak_maintained.rb new file mode 100644 index 0000000..7a77210 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/gamification/on_streak_maintained.rb @@ -0,0 +1,16 @@ +module Gamification + class OnStreakMaintained + def call(event) + profile_id = event.data[:profile_id] + streak = event.data[:streak] + + game_profile = GameProfileReadModel.find_by(profile_id: profile_id) + if game_profile + game_profile.update(streak: streak) + Rails.logger.info "Updated streak to #{streak} for GameProfile #{profile_id}" + else + Rails.logger.warn "GameProfile #{profile_id} not found for StreakMaintained event" + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/on_tier_achieved.rb b/apps/govquests-api/rails_app/app/read_models/gamification/on_tier_achieved.rb new file mode 100644 index 0000000..eab6486 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/gamification/on_tier_achieved.rb @@ -0,0 +1,16 @@ +module Gamification + class OnTierAchieved + def call(event) + profile_id = event.data[:profile_id] + tier = event.data[:tier] + + game_profile = GameProfileReadModel.find_by(profile_id: profile_id) + if game_profile + game_profile.update(tier: tier) + Rails.logger.info "Updated tier to #{tier} for GameProfile #{profile_id}" + else + Rails.logger.warn "GameProfile #{profile_id} not found for TierAchieved event" + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/on_track_completed.rb b/apps/govquests-api/rails_app/app/read_models/gamification/on_track_completed.rb new file mode 100644 index 0000000..dd07287 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/gamification/on_track_completed.rb @@ -0,0 +1,16 @@ +module Gamification + class OnTrackCompleted + def call(event) + profile_id = event.data[:profile_id] + track = event.data[:track] + + game_profile = GameProfileReadModel.find_by(profile_id: profile_id) + if game_profile + game_profile.update(track: track) + Rails.logger.info "Updated track to #{track} for GameProfile #{profile_id}" + else + Rails.logger.warn "GameProfile #{profile_id} not found for TrackCompleted event" + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/gamification/read_model_configuration.rb new file mode 100644 index 0000000..b22a1c6 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/gamification/read_model_configuration.rb @@ -0,0 +1,44 @@ +module Gamification + class GameProfileReadModel < ApplicationRecord + self.table_name = "user_game_profiles" + + validates :profile_id, presence: true, uniqueness: true + end + + # app/models/gamification/leaderboard_read_model.rb + + class LeaderboardReadModel < ApplicationRecord + self.table_name = "leaderboards" + self.primary_key = "leaderboard_id" + + has_many :leaderboard_entries, + foreign_key: "leaderboard_id", + primary_key: "leaderboard_id", + class_name: "Gamification::LeaderboardEntryReadModel", + inverse_of: :leaderboard + end + + class LeaderboardEntryReadModel < ApplicationRecord + self.table_name = "leaderboard_entries" + query_constraints :leaderboard_id, :profile_id + + belongs_to :leaderboard, + foreign_key: "leaderboard_id", + primary_key: "leaderboard_id", + class_name: "Gamification::LeaderboardReadModel", + inverse_of: :leaderboard_entries + + validates :rank, presence: true + validates :score, presence: true + end + + class ReadModelConfiguration + def call(event_store) + event_store.subscribe(OnTierAchieved, to: [ Gamification::TierAchieved ]) + event_store.subscribe(OnTrackCompleted, to: [ Gamification::TrackCompleted ]) + event_store.subscribe(OnStreakMaintained, to: [ Gamification::StreakMaintained ]) + event_store.subscribe(OnBadgeEarned, to: [ Gamification::BadgeEarned ]) + event_store.subscribe(OnLeaderboardUpdated, to: [ Gamification::LeaderboardUpdated ]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb b/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb deleted file mode 100644 index c7c61eb..0000000 --- a/apps/govquests-api/rails_app/app/read_models/notifications/configuration.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Notifications - class NotificationReadModel < ActiveRecord - end - - class Configuration - def call(event_store, command_bus) - event_store.subscribe(OnNotificationCreated, to: [ Notifications::NotificationCreated ]) - end - end -end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_created.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_created.rb index a934a26..1fd336a 100644 --- a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_created.rb @@ -2,17 +2,21 @@ module Notifications class OnNotificationCreated def call(event) notification_id = event.data.fetch(:notification_id) + template_id = event.data.fetch(:template_id) + user_id = event.data.fetch(:user_id) + channel = event.data.fetch(:channel) + priority = event.data.fetch(:priority) content = event.data[:content] - priority = event.data[:priority] - channel = event.data[:channel] - template_id = event.data[:template_id] + notification_type = event.data[:notification_type] NotificationReadModel.find_or_create_by(notification_id: notification_id).update( - content: content, - priority: priority, - channel: channel, template_id: template_id, - status: "Created" + user_id: user_id, + channel: channel, + priority: priority, + content: content, + notification_type: notification_type, + status: "created" ) end end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_opened.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_opened.rb new file mode 100644 index 0000000..2529991 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_opened.rb @@ -0,0 +1,11 @@ +module Notifications + class OnNotificationOpened + def call(event) + notification_id = event.data.fetch(:notification_id) + opened_at = event.data.fetch(:opened_at) + + notification = NotificationReadModel.find_by(notification_id: notification_id) + notification.update(opened_at: opened_at, status: "opened") if notification + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_received.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_received.rb new file mode 100644 index 0000000..727c8f7 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_received.rb @@ -0,0 +1,11 @@ +module Notifications + class OnNotificationReceived + def call(event) + notification_id = event.data.fetch(:notification_id) + received_at = event.data.fetch(:received_at) + + notification = NotificationReadModel.find_by(notification_id: notification_id) + notification.update(received_at: received_at, status: "received") if notification + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_scheduled.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_scheduled.rb new file mode 100644 index 0000000..37285c0 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_scheduled.rb @@ -0,0 +1,11 @@ +module Notifications + class OnNotificationScheduled + def call(event) + notification_id = event.data.fetch(:notification_id) + scheduled_time = event.data.fetch(:scheduled_time) + + notification = NotificationReadModel.find_by(notification_id: notification_id) + notification.update(scheduled_time: scheduled_time, status: "scheduled") if notification + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_sent.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_sent.rb new file mode 100644 index 0000000..d2319d6 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_sent.rb @@ -0,0 +1,11 @@ +module Notifications + class OnNotificationSent + def call(event) + notification_id = event.data.fetch(:notification_id) + sent_at = event.data.fetch(:sent_at) + + notification = NotificationReadModel.find_by(notification_id: notification_id) + notification.update!(sent_at: sent_at, status: "sent") + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_template_created.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_template_created.rb new file mode 100644 index 0000000..2220990 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_template_created.rb @@ -0,0 +1,16 @@ +module Notifications + class OnTemplateCreated + def call(event) + template_id = event.data.fetch(:template_id) + name = event.data.fetch(:name) + content = event.data.fetch(:content) + template_type = event.data.fetch(:template_type) + + NotificationTemplateReadModel.find_or_create_by(template_id: template_id).update( + name: name, + content: content, + template_type: template_type + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_template_deleted.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_template_deleted.rb new file mode 100644 index 0000000..87047b2 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_template_deleted.rb @@ -0,0 +1,10 @@ +module Notifications + class OnTemplateDeleted + def call(event) + template_id = event.data.fetch(:template_id) + + template = NotificationTemplateReadModel.find_by(template_id: template_id) + template.destroy if template + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_template_updated.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_template_updated.rb new file mode 100644 index 0000000..d73ee67 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_template_updated.rb @@ -0,0 +1,19 @@ +module Notifications + class OnTemplateUpdated + def call(event) + template_id = event.data.fetch(:template_id) + name = event.data[:name] + content = event.data[:content] + type = event.data[:notification_type] + + template = NotificationTemplateReadModel.find_by(template_id: template_id) + return unless template + + template.update( + name: name || template.name, + content: content || template.content, + notification_type: type || template.type + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/notifications/read_model_configuration.rb new file mode 100644 index 0000000..bc8d084 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/notifications/read_model_configuration.rb @@ -0,0 +1,37 @@ +module Notifications + class NotificationReadModel < ApplicationRecord + self.table_name = "notifications" + + validates :notification_id, presence: true, uniqueness: true + validates :template_id, presence: true + validates :user_id, presence: true + validates :channel, presence: true + validates :priority, presence: true, numericality: { only_integer: true, greater_than: 0, less_than: 6 } + validates :status, presence: true, inclusion: { in: %w[created scheduled sent received opened] } + end + + class NotificationTemplateReadModel < ApplicationRecord + self.table_name = "notification_templates" + + validates :template_id, presence: true, uniqueness: true + validates :name, presence: true, uniqueness: true + validates :content, presence: true + validates :template_type, presence: true, inclusion: { in: [ "email", "SMS", "push" ] } + end + + class ReadModelConfiguration + def call(event_store) + # Notification Events + event_store.subscribe(OnNotificationCreated, to: [ Notifications::NotificationCreated ]) + event_store.subscribe(OnNotificationScheduled, to: [ Notifications::NotificationScheduled ]) + event_store.subscribe(OnNotificationSent, to: [ Notifications::NotificationSent ]) + event_store.subscribe(OnNotificationReceived, to: [ Notifications::NotificationReceived ]) + event_store.subscribe(OnNotificationOpened, to: [ Notifications::NotificationOpened ]) + + # NotificationTemplate Events + event_store.subscribe(OnTemplateCreated, to: [ Notifications::NotificationTemplateCreated ]) + event_store.subscribe(OnTemplateUpdated, to: [ Notifications::NotificationTemplateUpdated ]) + event_store.subscribe(OnTemplateDeleted, to: [ Notifications::NotificationTemplateDeleted ]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_user_started_quest.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_user_started_quest.rb new file mode 100644 index 0000000..3604e38 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_user_started_quest.rb @@ -0,0 +1,13 @@ +module Questing + class OnUserStartedQuest + def call(event) + quest_id = event.data[:quest_id] + user_id = event.data[:user_id] + + UserQuest.find_or_create_by(quest_id: quest_id, user_id: user_id).update( + status: "started", + started_at: Time.current + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb index 90310a3..f35916d 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb @@ -3,9 +3,14 @@ class QuestReadModel < ApplicationRecord self.table_name = "quests" end + class UserQuest < ApplicationRecord + self.table_name = "user_quests" + end + class ReadModelConfiguration def call(event_store) event_store.subscribe(OnQuestCreated, to: [ Questing::QuestCreated ]) + event_store.subscribe(OnUserStartedQuest, to: [ Questing::UserStartedQuest ]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_claimed.rb b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_claimed.rb new file mode 100644 index 0000000..eac726d --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_claimed.rb @@ -0,0 +1,12 @@ +module Rewarding + class OnRewardClaimed + def call(event) + reward = RewardReadModel.find_by(reward_id: event.data[:reward_id]) + if reward + reward.update(claimed: true, delivery_status: "Claimed") + else + Rails.logger.warn "Reward #{event.data[:reward_id]} not found for RewardClaimed event" + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_created.rb b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_created.rb new file mode 100644 index 0000000..3e7fe44 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_created.rb @@ -0,0 +1,17 @@ +module Rewarding + class OnRewardCreated + def call(event) + reward_id = event.data.fetch(:reward_id) + reward_type = event.data.fetch(:reward_type) + value = event.data.fetch(:value) + expiry_date = event.data[:expiry_date] + + reward = RewardReadModel.find_or_initialize_by(reward_id: reward_id) + reward.reward_type = reward_type + reward.value = value + reward.expiry_date = expiry_date + reward.delivery_status = "Pending" + reward.save! + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_expired.rb b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_expired.rb new file mode 100644 index 0000000..b39137e --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_expired.rb @@ -0,0 +1,12 @@ +module Rewarding + class OnRewardExpired + def call(event) + reward = RewardReadModel.find_by(reward_id: event.data[:reward_id]) + if reward + reward.update(delivery_status: "Expired") + else + Rails.logger.warn "Reward #{event.data[:reward_id]} not found for RewardExpired event" + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_inventory_depleted.rb b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_inventory_depleted.rb new file mode 100644 index 0000000..d497e91 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_inventory_depleted.rb @@ -0,0 +1,12 @@ +module Rewarding + class OnRewardInventoryDepleted + def call(event) + reward = RewardReadModel.find_by(reward_id: event.data[:reward_id]) + if reward + reward.update(delivery_status: "InventoryDepleted") + else + Rails.logger.warn "Reward #{event.data[:reward_id]} not found for RewardInventoryDepleted event" + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_issued.rb b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_issued.rb new file mode 100644 index 0000000..9cacbdc --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/rewarding/on_reward_issued.rb @@ -0,0 +1,12 @@ +module Rewarding + class OnRewardIssued + def call(event) + reward = RewardReadModel.find_by(reward_id: event.data[:reward_id]) + if reward + reward.update(issued_to: event.data[:user_id], delivery_status: "Issued") + else + Rails.logger.warn "Reward #{event.data[:reward_id]} not found for RewardIssued event" + end + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb new file mode 100644 index 0000000..c0d2aa4 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb @@ -0,0 +1,18 @@ +module Rewarding + class RewardReadModel < ApplicationRecord + self.table_name = "rewards" + + validates :reward_id, presence: true, uniqueness: true + # Other validations... + end + + class ReadModelConfiguration + def call(event_store) + event_store.subscribe(OnRewardCreated, to: [ Rewarding::RewardCreated ]) + event_store.subscribe(OnRewardIssued, to: [ Rewarding::RewardIssued ]) + event_store.subscribe(OnRewardClaimed, to: [ Rewarding::RewardClaimed ]) + event_store.subscribe(OnRewardExpired, to: [ Rewarding::RewardExpired ]) + event_store.subscribe(OnRewardInventoryDepleted, to: [ Rewarding::RewardInventoryDepleted ]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/single_table_read_model.rb b/apps/govquests-api/rails_app/app/read_models/single_table_read_model.rb index f00d917..3a7d080 100644 --- a/apps/govquests-api/rails_app/app/read_models/single_table_read_model.rb +++ b/apps/govquests-api/rails_app/app/read_models/single_table_read_model.rb @@ -16,7 +16,7 @@ def subscribe_copy(event, sequence_of_keys, column = Array(sequence_of_keys).joi private def create_handler(event) - handler_class_name = "Create#{@active_record_name.name.gsub('::', '')}On#{event.name.gsub('::', '')}" + handler_class_name = "Create#{@active_record_name.name.gsub("::", "")}On#{event.name.gsub("::", "")}" Object.send(:remove_const, handler_class_name) if self.class.const_defined?(handler_class_name) _active_record_name_, _id_column_, _event_store_ = @active_record_name, @id_column, @event_store Object.const_set( @@ -30,7 +30,7 @@ def create_handler(event) end def copy_handler(event, sequence_of_keys, column) - handler_class_name = "Set#{@active_record_name.name.gsub('::', '')}#{column.to_s.camelcase}On#{event.name.gsub('::', '')}" + handler_class_name = "Set#{@active_record_name.name.gsub("::", "")}#{column.to_s.camelcase}On#{event.name.gsub("::", "")}" Object.send(:remove_const, handler_class_name) if self.class.const_defined?(handler_class_name) _active_record_name_, _id_column_, _event_store_ = @active_record_name, @id_column, @event_store Object.const_set( @@ -47,37 +47,25 @@ def copy_handler(event, sequence_of_keys, column) end class ReadModelHandler - def initialize(*args) - if args.present? - @event_store = args[0] - @active_record_name = args[1] - @id_column = args[2] - end - super() + def initialize(event_store, active_record_name, id_column) + @event_store = event_store + @active_record_name = active_record_name + @id_column = id_column end private attr_reader :active_record_name, :id_column, :event_store + # Concurrency-safe block for handling events def concurrent_safely(event) stream_name = "#{active_record_name}$#{record_id(event)}$#{event.event_type}" - read_scope = event_store.read.as_at.stream(stream_name) - begin - last_event = read_scope.last - return if last_event && last_event.timestamp > event.timestamp - ApplicationRecord.with_advisory_lock(active_record_name, record_id(event)) do - yield - event_store.link( - event.event_id, - stream_name: stream_name, - expected_version: last_event ? read_scope.to(last_event.event_id).count : -1 - ) - end - rescue RubyEventStore::WrongExpectedEventVersion - retry - rescue RubyEventStore::EventDuplicatedInStream + ApplicationRecord.with_advisory_lock(active_record_name, record_id(event)) do + yield + link_event_to_stream(event, stream_name) end + rescue RubyEventStore::WrongExpectedEventVersion, RubyEventStore::EventDuplicatedInStream + Rails.logger.warn "Event #{event.event_id} is already processed in stream #{stream_name}" end def find_or_initialize_record(event) @@ -87,23 +75,26 @@ def find_or_initialize_record(event) def record_id(event) event.data.fetch(id_column) end + + # Links the event to a stream for consistency + def link_event_to_stream(event, stream_name) + event_store.link(event.event_id, stream_name: stream_name) + end end class CreateRecord < ReadModelHandler def call(event) concurrent_safely(event) do - find_or_initialize_record(event).save + find_or_initialize_record(event).save! end end end class CopyEventAttribute < ReadModelHandler - def initialize(*args) - if args.present? - @sequence_of_keys = args[3] - @column = args[4] - end - super + def initialize(event_store, active_record_name, id_column, sequence_of_keys, column) + super(event_store, active_record_name, id_column) + @sequence_of_keys = sequence_of_keys + @column = column end def call(event) @@ -113,5 +104,6 @@ def call(event) end private + attr_reader :sequence_of_keys, :column end diff --git a/apps/govquests-api/rails_app/config/storage.yml b/apps/govquests-api/rails_app/config/storage.yml index 667f5e7..79d4004 100644 --- a/apps/govquests-api/rails_app/config/storage.yml +++ b/apps/govquests-api/rails_app/config/storage.yml @@ -23,7 +23,7 @@ local: # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage -# storage_account_name: your_account_name +# storage_user_name: your_user_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name-<%= Rails.env %> diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb b/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb index ec0e110..0eaf553 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb @@ -3,20 +3,29 @@ def change create_table :notifications do |t| t.string :notification_id, null: false, index: { unique: true } t.string :content, null: false - t.string :priority, null: false + t.integer :priority, null: false + t.string :template_id, null: false + t.string :user_id, null: false t.string :channel, null: false - t.string :template_id, null: true - t.string :status, default: "Pending" - t.datetime :scheduled_at, null: true + t.string :status, default: "created" + t.string :notification_type, null: false + t.datetime :scheduled_time + t.datetime :sent_at + t.datetime :received_at + t.datetime :opened_at t.timestamps end create_table :notification_templates do |t| t.string :template_id, null: false, index: { unique: true } - t.string :name, null: false + t.string :name, null: false, index: { unique: true } t.text :content, null: false - t.string :content_type, null: false, default: "text/plain" + t.string :template_type, null: false t.timestamps end + + add_index :notifications, :template_id + add_index :notifications, :user_id + add_index :notifications, :channel end end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb b/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb new file mode 100644 index 0000000..67d4042 --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb @@ -0,0 +1,14 @@ +class CreateRewards < ActiveRecord::Migration[8.0] + def change + create_table :rewards do |t| + t.string :reward_id, null: false, index: { unique: true } + t.string :reward_type, null: false + t.integer :value, null: false + t.datetime :expiry_date + t.string :issued_to + t.string :delivery_status, default: "Pending" + t.boolean :claimed, default: false + t.timestamps + end + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201750_create_user_quests.rb b/apps/govquests-api/rails_app/db/migrate/20240930201750_create_user_quests.rb new file mode 100644 index 0000000..bfb2f29 --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20240930201750_create_user_quests.rb @@ -0,0 +1,15 @@ +class CreateUserQuests < ActiveRecord::Migration[8.0] + def change + create_table :user_quests do |t| + t.string :quest_id, null: false + t.string :user_id, null: false + t.string :status, default: "started" + t.integer :progress_measure, default: 0 + t.datetime :started_at, null: true + t.datetime :completed_at, null: true + t.timestamps + end + + add_index :user_quests, %i[quest_id user_id], unique: true + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb b/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb new file mode 100644 index 0000000..42ea96b --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb @@ -0,0 +1,22 @@ +# rails_app/db/migrate/20240930201751_create_actions.rb +class CreateActions < ActiveRecord::Migration[8.0] + def change + create_table :actions do |t| + t.string :action_id, null: false, index: { unique: true } + t.string :content, null: false + t.string :priority, null: false + t.string :channel, null: false + t.string :status, default: "Pending" + t.timestamps + end + + create_table :action_logs do |t| + t.string :action_log_id, null: false, index: { unique: true } + t.string :action_id, null: false + t.string :user_id, null: false + t.datetime :executed_at, null: false + t.string :status, null: false + t.timestamps + end + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201752_create_user_game_profiles.rb b/apps/govquests-api/rails_app/db/migrate/20240930201752_create_user_game_profiles.rb new file mode 100644 index 0000000..1a0313e --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20240930201752_create_user_game_profiles.rb @@ -0,0 +1,30 @@ +class CreateUserGameProfiles < ActiveRecord::Migration[8.0] + def change + create_table :user_game_profiles do |t| + t.string :profile_id, null: false, index: { unique: true } + t.integer :tier, default: 0 + t.integer :track, default: 0 + t.integer :streak, default: 0 + t.integer :score, default: 0 + t.jsonb :badges, default: [] + t.timestamps + end + + create_table :leaderboards, id: false, primary_key: :leaderboard_id do |t| + t.string :leaderboard_id, null: false, index: { unique: true } + t.string :name, null: false + t.timestamps + end + + create_table :leaderboard_entries, id: false, primary_key: %i[leaderboard_id profile_id] do |t| + t.string :leaderboard_id, null: false + t.string :profile_id, null: false + t.integer :rank, null: false + t.integer :score, null: false + t.timestamps + end + + add_index :leaderboard_entries, %i[leaderboard_id profile_id], unique: true + add_foreign_key :leaderboard_entries, :leaderboards, column: :leaderboard_id, primary_key: :leaderboard_id + end +end diff --git a/apps/govquests-api/rails_app/db/schema.rb b/apps/govquests-api/rails_app/db/schema.rb index f96458f..62db998 100644 --- a/apps/govquests-api/rails_app/db/schema.rb +++ b/apps/govquests-api/rails_app/db/schema.rb @@ -10,10 +10,32 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_09_30_201737) do +ActiveRecord::Schema[8.0].define(version: 2024_09_30_201752) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" + create_table "action_logs", force: :cascade do |t| + t.string "action_log_id", null: false + t.string "action_id", null: false + t.string "user_id", null: false + t.datetime "executed_at", null: false + t.string "status", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["action_log_id"], name: "index_action_logs_on_action_log_id", unique: true + end + + create_table "actions", force: :cascade do |t| + t.string "action_id", null: false + t.string "content", null: false + t.string "priority", null: false + t.string "channel", null: false + t.string "status", default: "Pending" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["action_id"], name: "index_actions_on_action_id", unique: true + end + create_table "event_store_events", force: :cascade do |t| t.string "event_id", limit: 36, null: false t.string "event_type", null: false @@ -38,27 +60,54 @@ t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true end + create_table "leaderboard_entries", id: false, force: :cascade do |t| + t.string "leaderboard_id", null: false + t.string "profile_id", null: false + t.integer "rank", null: false + t.integer "score", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["leaderboard_id", "profile_id"], name: "index_leaderboard_entries_on_leaderboard_id_and_profile_id", unique: true + end + + create_table "leaderboards", id: false, force: :cascade do |t| + t.string "leaderboard_id", null: false + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["leaderboard_id"], name: "index_leaderboards_on_leaderboard_id", unique: true + end + create_table "notification_templates", force: :cascade do |t| t.string "template_id", null: false t.string "name", null: false t.text "content", null: false - t.string "content_type", default: "text/plain", null: false + t.string "template_type", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["name"], name: "index_notification_templates_on_name", unique: true t.index ["template_id"], name: "index_notification_templates_on_template_id", unique: true end create_table "notifications", force: :cascade do |t| t.string "notification_id", null: false t.string "content", null: false - t.string "priority", null: false + t.integer "priority", null: false + t.string "template_id", null: false + t.string "user_id", null: false t.string "channel", null: false - t.string "template_id" - t.string "status", default: "Pending" - t.datetime "scheduled_at" + t.string "status", default: "created" + t.string "notification_type", null: false + t.datetime "scheduled_time" + t.datetime "sent_at" + t.datetime "received_at" + t.datetime "opened_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["channel"], name: "index_notifications_on_channel" t.index ["notification_id"], name: "index_notifications_on_notification_id", unique: true + t.index ["template_id"], name: "index_notifications_on_template_id" + t.index ["user_id"], name: "index_notifications_on_user_id" end create_table "quests", force: :cascade do |t| @@ -76,6 +125,43 @@ t.index ["quest_id"], name: "index_quests_on_quest_id", unique: true end + create_table "rewards", force: :cascade do |t| + t.string "reward_id", null: false + t.string "reward_type", null: false + t.integer "value", null: false + t.datetime "expiry_date" + t.string "issued_to" + t.string "delivery_status", default: "Pending" + t.boolean "claimed", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["reward_id"], name: "index_rewards_on_reward_id", unique: true + end + + create_table "user_game_profiles", force: :cascade do |t| + t.string "profile_id", null: false + t.integer "tier", default: 0 + t.integer "track", default: 0 + t.integer "streak", default: 0 + t.integer "score", default: 0 + t.jsonb "badges", default: [] + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["profile_id"], name: "index_user_game_profiles_on_profile_id", unique: true + end + + create_table "user_quests", force: :cascade do |t| + t.string "quest_id", null: false + t.string "user_id", null: false + t.string "status", default: "started" + t.integer "progress_measure", default: 0 + t.datetime "started_at" + t.datetime "completed_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["quest_id", "user_id"], name: "index_user_quests_on_quest_id_and_user_id", unique: true + end + create_table "user_rewards", force: :cascade do |t| t.string "user_id", null: false t.string "reward_id", null: false @@ -111,4 +197,5 @@ end add_foreign_key "event_store_events_in_streams", "event_store_events", column: "event_id", primary_key: "event_id" + add_foreign_key "leaderboard_entries", "leaderboards", primary_key: "leaderboard_id" end diff --git a/apps/govquests-api/rails_app/lib/read_models_configuration.rb b/apps/govquests-api/rails_app/lib/read_models_configuration.rb index 24c3cf0..f884e23 100644 --- a/apps/govquests-api/rails_app/lib/read_models_configuration.rb +++ b/apps/govquests-api/rails_app/lib/read_models_configuration.rb @@ -7,8 +7,12 @@ def call(event_store, command_bus) enable_authentication_read_model(event_store) enable_quests_read_model(event_store) + enable_rewarding_read_model(event_store) + enable_action_tracking_read_model(event_store) + enable_gamification_read_model(event_store) + enable_notifications_read_model(event_store) - Govquests::Configuration.new.call(event_store, command_bus) + GovQuests::Configuration.new.call(event_store, command_bus) end private @@ -21,6 +25,22 @@ def enable_quests_read_model(event_store) Questing::ReadModelConfiguration.new.call(event_store) end + def enable_rewarding_read_model(event_store) + Rewarding::ReadModelConfiguration.new.call(event_store) + end + + def enable_action_tracking_read_model(event_store) + ActionTracking::ReadModelConfiguration.new.call(event_store) + end + + def enable_gamification_read_model(event_store) + Gamification::ReadModelConfiguration.new.call(event_store) + end + + def enable_notifications_read_model(event_store) + Notifications::ReadModelConfiguration.new.call(event_store) + end + def enable_res_infra_event_linking(event_store) [ RailsEventStore::LinkByEventType.new, diff --git a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb new file mode 100644 index 0000000..1762bf1 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb @@ -0,0 +1,32 @@ +require "test_helper" + +module ActionTracking + class OnActionCreatedTest < ActiveSupport::TestCase + def setup + @handler = OnActionCreated.new + @action_id = SecureRandom.uuid + @content = "Complete survey" + @priority = "High" + @channel = "Email" + end + + test "creates a new action when handling ActionCreated event" do + event = ActionCreated.new(data: { + action_id: @action_id, + content: @content, + priority: @priority, + channel: @channel + }) + + assert_difference "ActionReadModel.count", 1 do + @handler.call(event) + end + + action = ActionReadModel.find_by(action_id: @action_id) + assert_equal @content, action.content + assert_equal @priority, action.priority + assert_equal @channel, action.channel + assert_equal "Created", action.status + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb new file mode 100644 index 0000000..434f814 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +module ActionTracking + class OnActionExecutedTest < ActiveSupport::TestCase + def setup + @handler = OnActionExecuted.new + @action_log_id = SecureRandom.uuid + @action_id = SecureRandom.uuid + @user_id = SecureRandom.uuid + @timestamp = Time.current + end + + test "creates a new action log when handling ActionExecuted event" do + event = ActionExecuted.new(data: { + action_id: @action_id, + user_id: @user_id, + timestamp: @timestamp + }) + + assert_difference "ActionLogReadModel.count", 1 do + @handler.call(event) + end + + action_log = ActionLogReadModel.last + assert_equal @action_id, action_log.action_id + assert_equal @user_id, action_log.user_id + assert_equal @timestamp.to_i, action_log.executed_at.to_i + assert_equal "Executed", action_log.status + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_notification_created_test.rb b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_notification_created_test.rb new file mode 100644 index 0000000..3004051 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_notification_created_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +module Notifications + class OnNotificationCreatedTest < InMemoryTestCase + def setup + @handler = OnNotificationCreated.new + @notification_id = SecureRandom.uuid + @template_id = SecureRandom.uuid + @user_id = SecureRandom.uuid + @channel = "email" + @priority = 3 + @content = "You have a new message." + @notification_type = "email_notification" + end + + test "creates a new notification when handling NotificationCreated event" do + event = Notifications::NotificationCreated.new(data: { + notification_id: @notification_id, + template_id: @template_id, + user_id: @user_id, + channel: @channel, + priority: @priority, + content: @content, + notification_type: @notification_type + }) + + assert_difference "Notifications::NotificationReadModel.count", 1 do + @handler.call(event) + end + + notification = Notifications::NotificationReadModel.find_by(notification_id: @notification_id) + assert_equal @template_id, notification.template_id + assert_equal @user_id, notification.user_id + assert_equal @channel, notification.channel + assert_equal @priority, notification.priority + assert_equal @content, notification.content + assert_equal "created", notification.status + assert_equal @notification_type, notification.notification_type + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_template_created_test.rb b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_template_created_test.rb new file mode 100644 index 0000000..b482caf --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_template_created_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +module Notifications + class OnTemplateCreatedTest < ActiveSupport::TestCase + def setup + @handler = OnTemplateCreated.new + @template_id = SecureRandom.uuid + @name = "Welcome Email" + @content = "Welcome to our platform!" + @template_type = "email" + end + + test "creates a new notification template when handling NotificationTemplateCreated event" do + event = NotificationTemplateCreated.new(data: { + template_id: @template_id, + name: @name, + content: @content, + template_type: @template_type + }) + + assert_difference "NotificationTemplateReadModel.count", 1 do + @handler.call(event) + end + + template = NotificationTemplateReadModel.find_by(template_id: @template_id) + assert_equal @name, template.name + assert_equal @content, template.content + assert_equal @template_type, template.template_type + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb b/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb index b933d75..acc243e 100644 --- a/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb @@ -58,5 +58,29 @@ def setup assert_equal @wallet_address, existing_user.wallets.first["wallet_address"] assert_equal @chain_id, existing_user.wallets.first["chain_id"] end + + test "does not create duplicate users when handling UserRegistered event" do + existing_user = UserReadModel.create!( + user_id: @user_id, + email: @email, + user_type: @user_type, + wallets: [ { wallet_address: @wallet_address, chain_id: @chain_id } ] + ) + + event = UserRegistered.new(data: { + user_id: @user_id, + email: @email, + user_type: @user_type, + wallet_address: @wallet_address, + chain_id: @chain_id + }) + + assert_no_difference "UserReadModel.count" do + @handler.call(event) + end + + existing_user.reload + assert_equal @email, existing_user.email + end end end diff --git a/apps/govquests-api/rails_app/test/read_models/gamification/on_badge_earned_test.rb b/apps/govquests-api/rails_app/test/read_models/gamification/on_badge_earned_test.rb new file mode 100644 index 0000000..f6ae09c --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/gamification/on_badge_earned_test.rb @@ -0,0 +1,44 @@ +require "test_helper" + +module Gamification + class OnBadgeEarnedTest < ActiveSupport::TestCase + def setup + @handler = OnBadgeEarned.new + @profile_id = SecureRandom.uuid + @badge = "Achiever" + + # Create a game profile read model entry + GameProfileReadModel.create!( + profile_id: @profile_id, + badges: [] + ) + end + + test "adds a badge to game profile when handling BadgeEarned event" do + event = BadgeEarned.new(data: { + profile_id: @profile_id, + badge: @badge + }) + + @handler.call(event) + + profile = GameProfileReadModel.find_by(profile_id: @profile_id) + assert_includes profile.badges, @badge + end + + test "does not duplicate badges in game profile" do + profile = GameProfileReadModel.find_by(profile_id: @profile_id) + profile.update(badges: [ @badge ]) + + event = BadgeEarned.new(data: { + profile_id: @profile_id, + badge: @badge + }) + + @handler.call(event) + + profile.reload + assert_equal 1, profile.badges.count(@badge) + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/gamification/on_leaderboard_updated_test.rb b/apps/govquests-api/rails_app/test/read_models/gamification/on_leaderboard_updated_test.rb new file mode 100644 index 0000000..3de2bb8 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/gamification/on_leaderboard_updated_test.rb @@ -0,0 +1,37 @@ +require "test_helper" + +module Gamification + class OnLeaderboardUpdatedTest < ActiveSupport::TestCase + def setup + @handler = OnLeaderboardUpdated.new + @leaderboard_id = SecureRandom.uuid + @profile_id = SecureRandom.uuid + @score = 1500 + + @leaderboard = LeaderboardReadModel.create!( + leaderboard_id: @leaderboard_id, + name: "Top Players" + ) + end + + test "updates leaderboard entry when handling LeaderboardUpdated event" do + event = LeaderboardUpdated.new(data: { + leaderboard_id: @leaderboard_id, + profile_id: @profile_id, + score: @score + }) + + assert_difference "LeaderboardEntryReadModel.count", 1 do + @handler.call(event) + end + + entry = LeaderboardEntryReadModel.find_by( + leaderboard_id: @leaderboard_id, + profile_id: @profile_id + ) + assert_equal @score, entry.score + assert_not_nil entry.rank + assert_equal @leaderboard, entry.leaderboard + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/gamification/on_tier_achieved_test.rb b/apps/govquests-api/rails_app/test/read_models/gamification/on_tier_achieved_test.rb new file mode 100644 index 0000000..22e0e02 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/gamification/on_tier_achieved_test.rb @@ -0,0 +1,27 @@ +require "test_helper" + +module Gamification + class OnTierAchievedTest < ActiveSupport::TestCase + def setup + @handler = OnTierAchieved.new + @profile_id = SecureRandom.uuid + @game_profile = GameProfileReadModel.create!(profile_id: @profile_id, tier: "1", score: 100) + end + + test "updates tier when handling TierAchieved event" do + event = Gamification::TierAchieved.new(data: { profile_id: @profile_id, tier: "2" }) + @handler.call(event) + + @game_profile.reload + assert_equal 2, @game_profile.tier + end + + test "handles non-existent game profile gracefully" do + non_existent_profile_id = SecureRandom.uuid + event = Gamification::TierAchieved.new(data: { profile_id: non_existent_profile_id, tier: "2" }) + @handler.call(event) + + assert_nil GameProfileReadModel.find_by(profile_id: non_existent_profile_id) + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/questing/on_user_started_quest_test.rb b/apps/govquests-api/rails_app/test/read_models/questing/on_user_started_quest_test.rb new file mode 100644 index 0000000..4eab2ac --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/questing/on_user_started_quest_test.rb @@ -0,0 +1,26 @@ +require "test_helper" + +module Questing + class OnUserStartedQuestTest < ActiveSupport::TestCase + def setup + @handler = Questing::OnUserStartedQuest.new + @quest_id = SecureRandom.uuid + @user_id = SecureRandom.uuid + end + + test "creates or updates user quest when handling UserStartedQuest event" do + event = Questing::UserStartedQuest.new(data: { + quest_id: @quest_id, + user_id: @user_id + }) + + assert_difference "UserQuest.count", 1 do + @handler.call(event) + end + + user_quest = UserQuest.find_by(quest_id: @quest_id, user_id: @user_id) + assert_equal "started", user_quest.status + assert_not_nil user_quest.started_at + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_claimed_test.rb b/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_claimed_test.rb new file mode 100644 index 0000000..b1c5541 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_claimed_test.rb @@ -0,0 +1,34 @@ +require "test_helper" + +module Rewarding + class OnRewardClaimedTest < ActiveSupport::TestCase + def setup + @handler = OnRewardClaimed.new + @reward_id = SecureRandom.uuid + @user_id = SecureRandom.uuid + + # Create a reward read model entry + RewardReadModel.create!( + reward_id: @reward_id, + reward_type: "points", + value: 100, + delivery_status: "Issued", + issued_to: @user_id, + claimed: false + ) + end + + test "updates reward status to claimed when handling RewardClaimed event" do + event = RewardClaimed.new(data: { + reward_id: @reward_id, + user_id: @user_id + }) + + @handler.call(event) + + reward = RewardReadModel.find_by(reward_id: @reward_id) + assert_equal true, reward.claimed + assert_equal "Claimed", reward.delivery_status + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_created_test.rb b/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_created_test.rb new file mode 100644 index 0000000..485a68f --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_created_test.rb @@ -0,0 +1,30 @@ +module Rewarding + class OnRewardCreatedTest < ActiveSupport::TestCase + def setup + @handler = OnRewardCreated.new + @reward_id = SecureRandom.uuid + @reward_type = "points" + @value = 100 + @expiry_date = Time.current + 7.days + end + + test "creates a new reward when handling RewardCreated event" do + event = RewardCreated.new(data: { + reward_id: @reward_id, + reward_type: @reward_type, + value: @value, + expiry_date: @expiry_date + }) + + assert_difference "RewardReadModel.count", 1 do + @handler.call(event) + end + + reward = RewardReadModel.find_by(reward_id: @reward_id) + assert_equal @reward_type, reward.reward_type + assert_equal @value, reward.value + assert_equal @expiry_date.to_i, reward.expiry_date.to_i + assert_equal "Pending", reward.delivery_status + end + end +end diff --git a/apps/govquests-api/rails_app/test/test_helper.rb b/apps/govquests-api/rails_app/test/test_helper.rb index b757c3c..01efd64 100644 --- a/apps/govquests-api/rails_app/test/test_helper.rb +++ b/apps/govquests-api/rails_app/test/test_helper.rb @@ -13,7 +13,7 @@ def before_setup Rails.configuration.event_store = Infra::EventStore.in_memory Rails.configuration.command_bus = Arkency::CommandBus.new - Configuration.new.call( + ::ReadModelsConfiguration.new.call( Rails.configuration.event_store, Rails.configuration.command_bus ) result @@ -82,11 +82,11 @@ def supply_product(product_id, quantity) def update_price(product_id, new_price) patch "/products/#{product_id}", - params: { - "authenticity_token" => "[FILTERED]", - "product_id" => product_id, - price: new_price - } + params: { + "authenticity_token" => "[FILTERED]", + "product_id" => product_id, + :price => new_price + } end def login(client_id) @@ -96,12 +96,12 @@ def login(client_id) def submit_order(customer_id, order_id) post "/orders", - params: { - "authenticity_token" => "[FILTERED]", - "order_id" => order_id, - "customer_id" => customer_id, - "commit" => "Submit order" - } + params: { + "authenticity_token" => "[FILTERED]", + "order_id" => order_id, + "customer_id" => customer_id, + "commit" => "Submit order" + } end def visit_customers_index From 6f815f0ae84d0b9fb3199241030f2df16ddbbf29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Fri, 4 Oct 2024 10:00:18 -0300 Subject: [PATCH 11/17] feat: use readmodel in quests endpoint --- .../action_tracking/lib/action_tracking.rb | 2 +- .../lib/action_tracking/action.rb | 25 +- .../lib/action_tracking/commands.rb | 9 +- .../lib/action_tracking/events.rb | 6 +- .../lib/action_tracking/on_action_commands.rb | 6 +- .../lib/action_tracking/value_objects.rb | 10 - .../action_tracking/test/action_test.rb | 4 +- .../lib/gamification/value_objects.rb | 9 - .../lib/notifications/value_objects.rb | 71 ------ .../govquests/questing/lib/questing.rb | 1 - .../questing/lib/questing/commands.rb | 18 +- .../govquests/questing/lib/questing/events.rb | 26 +-- .../lib/questing/on_quest_commands.rb | 13 +- .../govquests/questing/lib/questing/quest.rb | 35 ++- .../questing/lib/questing/value_objects.rb | 27 --- .../govquests/questing/test/quest_test.rb | 35 ++- .../rewarding/lib/rewarding/value_objects.rb | 21 -- apps/govquests-api/rails_app/Gemfile | 2 +- apps/govquests-api/rails_app/Gemfile.lock | 3 + .../action_completions_controller.rb | 48 ++++ .../app/controllers/quests_controller.rb | 213 ++++-------------- .../action_tracking/on_action_created.rb | 11 +- .../action_tracking/on_action_executed.rb | 3 + .../read_model_configuration.rb | 3 - .../on_action_associated_with_quest.rb | 21 ++ .../read_models/questing/on_quest_created.rb | 24 +- .../questing/read_model_configuration.rb | 10 +- .../rails_app/config/application.rb | 2 + .../rails_app/config/initializers/cors.rb | 2 +- apps/govquests-api/rails_app/config/routes.rb | 7 +- .../migrate/20240930201726_create_quests.rb | 12 +- .../migrate/20240930201751_create_actions.rb | 15 +- .../migrate/20241001131854_create_quests.rb | 13 -- ...5857_add_completion_data_to_action_logs.rb | 5 + apps/govquests-api/rails_app/db/schema.rb | 30 ++- apps/govquests-api/rails_app/db/seeds.rb | 137 ++++++++++- .../action_tracking/on_action_created_test.rb | 13 +- .../on_action_executed_test.rb | 5 +- .../on_quest_associated_with_action_test.rb | 32 +++ .../questing/on_quest_created_test.rb | 70 ++---- .../questing/on_user_started_quest_test.rb | 44 ++-- .../rewarding/on_reward_created_test.rb | 3 + 42 files changed, 466 insertions(+), 580 deletions(-) delete mode 100644 apps/govquests-api/govquests/action_tracking/lib/action_tracking/value_objects.rb delete mode 100644 apps/govquests-api/govquests/gamification/lib/gamification/value_objects.rb delete mode 100644 apps/govquests-api/govquests/notifications/lib/notifications/value_objects.rb delete mode 100644 apps/govquests-api/govquests/questing/lib/questing/value_objects.rb delete mode 100644 apps/govquests-api/govquests/rewarding/lib/rewarding/value_objects.rb create mode 100644 apps/govquests-api/rails_app/app/controllers/action_completions_controller.rb create mode 100644 apps/govquests-api/rails_app/app/read_models/questing/on_action_associated_with_quest.rb delete mode 100644 apps/govquests-api/rails_app/db/migrate/20241001131854_create_quests.rb create mode 100644 apps/govquests-api/rails_app/db/migrate/20241004125857_add_completion_data_to_action_logs.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/questing/on_quest_associated_with_action_test.rb diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking.rb index 0c5a62f..d1dafc1 100644 --- a/apps/govquests-api/govquests/action_tracking/lib/action_tracking.rb +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking.rb @@ -9,7 +9,7 @@ class Configuration def call(event_store, command_bus) command_handler = OnActionCommands.new(event_store) command_bus.register(CreateAction, command_handler) - command_bus.register(ExecuteAction, command_handler) + command_bus.register(CompleteAction, command_handler) end end end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb index c38239d..089623e 100644 --- a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb @@ -4,42 +4,33 @@ class Action def initialize(id) @id = id - @content = nil - @priority = nil - @channel = nil - @executions = [] end - def create(content, priority, channel) + def create(content, action_type, completion_criteria) apply ActionCreated.new(data: { action_id: @id, content: content, - priority: priority, - channel: channel + action_type: action_type, + completion_criteria: completion_criteria }) end - def execute(user_id, timestamp) + def complete(user_id, completion_data) apply ActionExecuted.new(data: { action_id: @id, user_id: user_id, - timestamp: timestamp + completion_data: completion_data }) end - private - on ActionCreated do |event| @content = event.data[:content] - @priority = event.data[:priority] - @channel = event.data[:channel] + @action_type = event.data[:action_type] + @completion_criteria = event.data[:completion_criteria] end on ActionExecuted do |event| - @executions << { - user_id: event.data[:user_id], - timestamp: event.data[:timestamp] - } + # Handle completion logic if needed end end end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/commands.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/commands.rb index 3f2e0b2..7e7b704 100644 --- a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/commands.rb +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/commands.rb @@ -1,17 +1,18 @@ +# govquests/action_tracking/lib/action_tracking/commands.rb module ActionTracking class CreateAction < Infra::Command attribute :action_id, Infra::Types::UUID attribute :content, Infra::Types::String - attribute :priority, Infra::Types::String - attribute :channel, Infra::Types::String + attribute :action_type, Infra::Types::String + attribute :completion_criteria, Infra::Types::Hash alias_method :aggregate_id, :action_id end - class ExecuteAction < Infra::Command + class CompleteAction < Infra::Command attribute :action_id, Infra::Types::UUID attribute :user_id, Infra::Types::UUID - attribute :timestamp, Infra::Types::Time + attribute :completion_data, Infra::Types::Hash alias_method :aggregate_id, :action_id end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/events.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/events.rb index 4b808ba..cacbec1 100644 --- a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/events.rb +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/events.rb @@ -2,13 +2,13 @@ module ActionTracking class ActionCreated < Infra::Event attribute :action_id, Infra::Types::UUID attribute :content, Infra::Types::String - attribute :priority, Infra::Types::String - attribute :channel, Infra::Types::String + attribute :action_type, Infra::Types::String + attribute :completion_criteria, Infra::Types::Hash end class ActionExecuted < Infra::Event attribute :action_id, Infra::Types::UUID attribute :user_id, Infra::Types::UUID - attribute :timestamp, Infra::Types::Time + attribute :completion_data, Infra::Types::Hash end end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb index b6d408f..98fd076 100644 --- a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb @@ -8,9 +8,9 @@ def call(command) @repository.with_aggregate(Action, command.aggregate_id) do |action| case command when CreateAction - action.create(command.content, command.priority, command.channel) - when ExecuteAction - action.execute(command.user_id, command.timestamp) + action.create(command.content, command.action_type, command.completion_criteria) + when CompleteAction + action.complete(command.user_id, command.completion_data) end end end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/value_objects.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/value_objects.rb deleted file mode 100644 index ec493cf..0000000 --- a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/value_objects.rb +++ /dev/null @@ -1,10 +0,0 @@ -# govquests/action_tracking/lib/action_tracking/value_objects.rb -module ActionTracking - class ActionPriority < Dry::Struct - attribute :priority, Infra::Types::String.enum("Low", "Medium", "High") - end - - class ActionChannel < Dry::Struct - attribute :channel, Infra::Types::String.enum("Email", "SMS", "Push") - end -end diff --git a/apps/govquests-api/govquests/action_tracking/test/action_test.rb b/apps/govquests-api/govquests/action_tracking/test/action_test.rb index b2d1967..b374ca6 100644 --- a/apps/govquests-api/govquests/action_tracking/test/action_test.rb +++ b/apps/govquests-api/govquests/action_tracking/test/action_test.rb @@ -12,10 +12,9 @@ def setup def test_create_a_new_action content = "Complete survey" - priority = "High" channel = "Email" - @action.create(content, priority, channel) + @action.create(content, channel) events = @action.unpublished_events.to_a assert_equal 1, events.size @@ -23,7 +22,6 @@ def test_create_a_new_action assert_instance_of ActionCreated, event assert_equal @action_id, event.data[:action_id] assert_equal content, event.data[:content] - assert_equal priority, event.data[:priority] assert_equal channel, event.data[:channel] end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/value_objects.rb b/apps/govquests-api/govquests/gamification/lib/gamification/value_objects.rb deleted file mode 100644 index 2b0043a..0000000 --- a/apps/govquests-api/govquests/gamification/lib/gamification/value_objects.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gamification - class RewardType < Dry::Struct - values :points, :experience, :badge, :token - end - - class RewardDeliveryStatus < Dry::Struct - values :pending, :issued, :claimed, :expired - end -end diff --git a/apps/govquests-api/govquests/notifications/lib/notifications/value_objects.rb b/apps/govquests-api/govquests/notifications/lib/notifications/value_objects.rb deleted file mode 100644 index 208d5de..0000000 --- a/apps/govquests-api/govquests/notifications/lib/notifications/value_objects.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Notifications - class NotificationStatus < Dry::Struct - attribute :status, Infra::Types::String - - def self.created - new(status: "created") - end - - def self.scheduled - new(status: "scheduled") - end - - def self.sent - new(status: "sent") - end - - def self.received - new(status: "received") - end - - def self.opened - new(status: "opened") - end - end - - class NotificationType < Dry::Struct - attribute :notification_type, Infra::Types::String - end - - class NotificationContent < Dry::Struct - attribute :content, Infra::Types::String - - def initialize(content) - super - raise "Content cannot be empty" if content.strip.empty? - end - end - - class NotificationChannel < Dry::Struct - attribute :channel, Infra::Types::String - - VALID_CHANNELS = ["email", "SMS", "push"] - - def initialize(channel) - super - raise "Invalid channel" unless VALID_CHANNELS.include?(channel) - end - end - - class NotificationPriority < Dry::Struct - attribute :priority, Infra::Types::Integer.constrained(gt: 0, lt: 6) # 1 (low) to 5 (high) - end - - class TemplateName < Dry::Struct - attribute :name, Infra::Types::String - - def initialize(name) - super - raise "Template name cannot be empty" if name.strip.empty? - end - end - - class TemplateContent < Dry::Struct - attribute :content, Infra::Types::String - - def initialize(content) - super - raise "Template content cannot be empty" if content.strip.empty? - end - end -end diff --git a/apps/govquests-api/govquests/questing/lib/questing.rb b/apps/govquests-api/govquests/questing/lib/questing.rb index 5b36034..6df7f9c 100644 --- a/apps/govquests-api/govquests/questing/lib/questing.rb +++ b/apps/govquests-api/govquests/questing/lib/questing.rb @@ -10,7 +10,6 @@ def call(event_store, command_bus) command_handler = OnQuestCommands.new(event_store) command_bus.register(CreateQuest, command_handler) command_bus.register(AssociateActionWithQuest, command_handler) - command_bus.register(UpdateQuestProgress, command_handler) end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands.rb b/apps/govquests-api/govquests/questing/lib/questing/commands.rb index d5fb1d7..a7b2c70 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/commands.rb @@ -1,12 +1,11 @@ module Questing class CreateQuest < Infra::Command attribute :quest_id, Infra::Types::UUID - attribute :audience, Infra::Types::String + attribute :title, Infra::Types::String + attribute :intro, Infra::Types::String attribute :quest_type, Infra::Types::String - attribute :duration, Infra::Types::Integer - attribute :difficulty, Infra::Types::String - attribute :requirements, Infra::Types::Array.optional - attribute :reward, Infra::Types::Hash.optional + attribute :audience, Infra::Types::String + attribute :reward, Infra::Types::Hash alias_method :aggregate_id, :quest_id end @@ -14,14 +13,7 @@ class CreateQuest < Infra::Command class AssociateActionWithQuest < Infra::Command attribute :quest_id, Infra::Types::UUID attribute :action_id, Infra::Types::UUID - - alias_method :aggregate_id, :quest_id - end - - class UpdateQuestProgress < Infra::Command - attribute :quest_id, Infra::Types::UUID - attribute :user_id, Infra::Types::UUID - attribute :action_id, Infra::Types::UUID + attribute :position, Infra::Types::Integer alias_method :aggregate_id, :quest_id end diff --git a/apps/govquests-api/govquests/questing/lib/questing/events.rb b/apps/govquests-api/govquests/questing/lib/questing/events.rb index 87b6a2d..de31226 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/events.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/events.rb @@ -1,32 +1,16 @@ module Questing class QuestCreated < Infra::Event attribute :quest_id, Infra::Types::UUID - attribute :audience, Infra::Types::String + attribute :title, Infra::Types::String + attribute :intro, Infra::Types::String attribute :quest_type, Infra::Types::String - attribute :duration, Infra::Types::Integer - attribute :difficulty, Infra::Types::String - attribute :requirements, Infra::Types::Array.optional - attribute :reward, Infra::Types::Hash.optional - attribute :subquests, Infra::Types::Array.optional - end - - class UserStartedQuest < Infra::Event - attribute :quest_id, Infra::Types::UUID - attribute :user_id, Infra::Types::UUID - end - - class QuestRequirementAdded < Infra::Event - attribute :quest_id, Infra::Types::UUID - attribute :requirement, Infra::Types::Hash - end - - class SubquestAdded < Infra::Event - attribute :quest_id, Infra::Types::UUID - attribute :subquest, Infra::Types::Hash + attribute :audience, Infra::Types::String + attribute :reward, Infra::Types::Hash end class ActionAssociatedWithQuest < Infra::Event attribute :quest_id, Infra::Types::UUID attribute :action_id, Infra::Types::UUID + attribute :position, Infra::Types::Integer end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb index 06d31de..fc27a55 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb @@ -8,18 +8,9 @@ def call(command) @repository.with_aggregate(Quest, command.aggregate_id) do |quest| case command when CreateQuest - quest.create( - command.audience, - command.quest_type, - command.duration, - command.difficulty, - command.requirements, - command.reward - ) + quest.create(command.title, command.intro, command.quest_type, command.audience, command.reward) when AssociateActionWithQuest - quest.associate_action(command.action_id) - when UpdateQuestProgress - quest.update_progress(command.user_id, command.action_id) + quest.associate_action(command.action_id, command.position) end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest.rb b/apps/govquests-api/govquests/questing/lib/questing/quest.rb index 023b456..e74468b 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/quest.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/quest.rb @@ -4,47 +4,38 @@ class Quest def initialize(id) @id = id - @status = "created" + @actions = [] end - def create(audience, quest_type, duration, difficulty, requirements = [], reward = {}, subquests = []) - raise "Quest already created" unless @status == "created" - + def create(title, intro, quest_type, audience, reward) apply QuestCreated.new(data: { quest_id: @id, - audience: audience, + title: title, + intro: intro, quest_type: quest_type, - duration: duration, - difficulty: difficulty, - requirements: requirements, - reward: reward, - subquests: subquests + audience: audience, + reward: reward }) end - def associate_action(action_id) + def associate_action(action_id, position) apply ActionAssociatedWithQuest.new(data: { quest_id: @id, - action_id: action_id + action_id: action_id, + position: position }) end - private - on QuestCreated do |event| - @audience = event.data[:audience] + @title = event.data[:title] + @intro = event.data[:intro] @quest_type = event.data[:quest_type] - @duration = event.data[:duration] - @difficulty = event.data[:difficulty] - @requirements = event.data[:requirements] + @audience = event.data[:audience] @reward = event.data[:reward] - @subquests = event.data[:subquests] - @status = "active" # Change status to prevent re-creation end on ActionAssociatedWithQuest do |event| - @actions ||= [] - @actions << event.data[:action_id] + @actions << {id: event.data[:action_id], position: event.data[:position]} end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb b/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb deleted file mode 100644 index 20b24e9..0000000 --- a/apps/govquests-api/govquests/questing/lib/questing/value_objects.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Questing - class QuestAudience < Dry::Struct - values :AllUsers, :Delegates, :NewUsers - end - - class QuestType < Dry::Struct - values :Standard, :Epic, :Legendary - end - - class QuestStatus < Dry::Struct - values :Created, :Started, :Completed, :Expired, :Archived - end - - class QuestDifficulty < Dry::Struct - values :Easy, :Medium, :Hard, :Expert - end - - class QuestReward < Dry::Struct - attribute :reward_type, Infra::Types::String - attribute :reward_value, Infra::Types::Integer - end - - class QuestRequirement < Dry::Struct - attribute :quest_type, Infra::Types::String - attribute :description, Infra::Types::String - end -end diff --git a/apps/govquests-api/govquests/questing/test/quest_test.rb b/apps/govquests-api/govquests/questing/test/quest_test.rb index ee7fd00..098a81e 100644 --- a/apps/govquests-api/govquests/questing/test/quest_test.rb +++ b/apps/govquests-api/govquests/questing/test/quest_test.rb @@ -1,3 +1,4 @@ +# govquests/questing/test/quest_test.rb require_relative "test_helper" module Questing @@ -11,43 +12,32 @@ def setup end def test_create_a_new_quest - audience = "AllUsers" + title = "Governance 101" + intro = "Learn about governance basics" quest_type = "Standard" - duration = 7 - difficulty = "Medium" - requirements = [{quest_type: "action", description: "Complete action X"}] - reward = {quest_type: "points", value: 100} - subquests = [{id: SecureRandom.uuid, description: "Subquest 1"}] + audience = "AllUsers" + reward = {type: "points", value: 100} - @quest.create(audience, quest_type, duration, difficulty, requirements, reward, subquests) + @quest.create(title, intro, quest_type, audience, reward) events = @quest.unpublished_events.to_a assert_equal 1, events.size event = events.first assert_instance_of QuestCreated, event assert_equal @quest_id, event.data[:quest_id] - assert_equal audience, event.data[:audience] + assert_equal title, event.data[:title] + assert_equal intro, event.data[:intro] assert_equal quest_type, event.data[:quest_type] - assert_equal duration, event.data[:duration] - assert_equal difficulty, event.data[:difficulty] - assert_equal requirements, event.data[:requirements] + assert_equal audience, event.data[:audience] assert_equal reward, event.data[:reward] - assert_equal subquests, event.data[:subquests] - end - - def test_cannot_create_an_already_created_quest - @quest.create("AllUsers", "Standard", 7, "Medium") - - assert_raises(RuntimeError, "Quest already created") do - @quest.create("Delegates", "Epic", 30, "Hard") - end end def test_associate_an_action_with_a_quest - @quest.create("AllUsers", "Standard", 7, "Medium") + @quest.create("Test Quest", "Test Intro", "Standard", "AllUsers", {type: "points", value: 10}) action_id = SecureRandom.uuid + position = 1 - @quest.associate_action(action_id) + @quest.associate_action(action_id, position) events = @quest.unpublished_events.to_a assert_equal 2, events.size @@ -55,6 +45,7 @@ def test_associate_an_action_with_a_quest assert_instance_of ActionAssociatedWithQuest, event assert_equal @quest_id, event.data[:quest_id] assert_equal action_id, event.data[:action_id] + assert_equal position, event.data[:position] end end end diff --git a/apps/govquests-api/govquests/rewarding/lib/rewarding/value_objects.rb b/apps/govquests-api/govquests/rewarding/lib/rewarding/value_objects.rb deleted file mode 100644 index 74ebd41..0000000 --- a/apps/govquests-api/govquests/rewarding/lib/rewarding/value_objects.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Rewarding - class RewardType < Dry::Struct - values :Experience, :Attribute, :Points - end - - class RewardValue < Dry::Struct - attribute :amount, Infra::Types::Integer - end - - class RewardExpiryDate < Dry::Struct - attribute :expiry_date, Infra::Types::Time - end - - class Amount < Dry::Struct - attribute :value, Infra::Types::Integer - end - - class RewardDeliveryStatus < Dry::Struct - values :Pending, :Issued, :Claimed, :Expired - end -end diff --git a/apps/govquests-api/rails_app/Gemfile b/apps/govquests-api/rails_app/Gemfile index 31841b7..8cf4736 100644 --- a/apps/govquests-api/rails_app/Gemfile +++ b/apps/govquests-api/rails_app/Gemfile @@ -32,7 +32,7 @@ gem "thruster", require: false # gem "image_processing", "~> 1.2" # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible -# gem "rack-cors" +gem "rack-cors" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem diff --git a/apps/govquests-api/rails_app/Gemfile.lock b/apps/govquests-api/rails_app/Gemfile.lock index cebafc8..d33752f 100644 --- a/apps/govquests-api/rails_app/Gemfile.lock +++ b/apps/govquests-api/rails_app/Gemfile.lock @@ -272,6 +272,8 @@ GEM raabro (1.4.0) racc (1.8.1) rack (3.1.7) + rack-cors (2.0.2) + rack (>= 2.0.0) rack-session (2.0.0) rack (>= 3.0.0) rack-test (2.1.0) @@ -437,6 +439,7 @@ DEPENDENCIES pry-meta pry-rails (~> 0.3.9) puma (>= 5.0) + rack-cors rails! rails_event_store (~> 2.15.0) rubocop diff --git a/apps/govquests-api/rails_app/app/controllers/action_completions_controller.rb b/apps/govquests-api/rails_app/app/controllers/action_completions_controller.rb new file mode 100644 index 0000000..3aac577 --- /dev/null +++ b/apps/govquests-api/rails_app/app/controllers/action_completions_controller.rb @@ -0,0 +1,48 @@ +class ActionCompletionsController < ApplicationController + def start + action = ActionTracking::ActionReadModel.find_by(action_id: params[:action_id]) + if action + token = generate_completion_token(action.action_id) + render json: { token: token, expires_at: 30.minutes.from_now } + else + render json: { error: "Action not found" }, status: :not_found + end + end + + def complete + action = ActionTracking::ActionReadModel.find_by(action_id: params[:action_id]) + if action && valid_completion_token?(params[:token], action.action_id) + command = ActionTracking::CompleteAction.new( + action_id: action.action_id, + user_id: current_user.id, + completion_data: params[:completion_data] + ) + Rails.configuration.command_bus.call(command) + render json: { message: "Action completed successfully" } + else + render json: { error: "Invalid completion attempt" }, status: :unprocessable_entity + end + end + + private + + def generate_completion_token(action_id) + expiration = 30.minutes.from_now.to_i + payload = { + action_id: action_id, + user_id: current_user.id, + exp: expiration + } + JWT.encode(payload, Rails.application.secrets.secret_key_base) + end + + def valid_completion_token?(token, action_id) + decoded_token = JWT.decode(token, Rails.application.secrets.secret_key_base) + payload = decoded_token.first + payload["action_id"] == action_id && + payload["user_id"] == current_user.id && + Time.at(payload["exp"]) > Time.now + rescue JWT::DecodeError + false + end +end diff --git a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb index 9d21485..b20ae12 100644 --- a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb +++ b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb @@ -1,182 +1,47 @@ -# app/controllers/quests_controller.rb class QuestsController < ApplicationController def index - quests = [ - { - id: "1", - img_url: "https://file.coinexstatic.com/2023-11-16/BB3FDB00283C55B4C36B94CFAC0C3271.png", - title: "Governance 101", - status: "start", - rewards: [ - { - type: "Points", - amount: 50 - }, - { - type: "OP", - amount: 2 - } - ], - intro: "First things first: understand what are the Optimism Values and what is expect of you in this important role.", - steps: [ - { - content: "Code of Conduct", - duration: 15, - }, - { - content: "Optimistic Vision", - duration: 10, - }, - { - content: "Working Constitution", - duration: 15, - }, - { - content: "Delegate Expectations", - duration: 12, - }, - ] - }, - { - id: "2", - img_url: "https://file.coinexstatic.com/2023-11-16/BB3FDB00283C55B4C36B94CFAC0C3271.png", - title: "Governance 101", - status: "start", - rewards: [ - { - type: "Points", - amount: 100 - } - ], - intro: "First things first: understand what are the Optimism Values and what is expect of you in this important role.", - steps: [ - { - content: "Code of Conduct", - duration: 15, - }, - { - content: "Optimistic Vision", - duration: 15, - - }, - { - content: "Working Constitution", - duration: 20, - }, - { - content: "Delegate Expectations", - duration: 25, - }, - ] - }, - { - id: "3", - img_url: "https://file.coinexstatic.com/2023-11-16/BB3FDB00283C55B4C36B94CFAC0C3271.png", - title: "Governance 101", - status: "start", - rewards: [ - { - type: "Points", - amount: 50 - }, - { - type: "OP", - amount: 2 - } - ], - intro: "First things first: understand what are the Optimism Values and what is expect of you in this important role.", - steps: [ - { - content: "Code of Conduct", - duration: 15, - }, - { - content: "Optimistic Vision", - duration: 10, - - }, - { - content: "Working Constitution", - duration: 15, - }, - { - content: "Delegate Expectations", - duration: 12, - }, - ] - }, - { - id: "4", - img_url: "https://file.coinexstatic.com/2023-11-16/BB3FDB00283C55B4C36B94CFAC0C3271.png", - title: "Governance 101", - status: "start", - rewards: [ - { - type: "Points", - amount: 100 - } - ], - intro: "First things first: understand what are the Optimism Values and what is expect of you in this important role.", - steps: [ - { - content: "Code of Conduct", - duration: 15, - }, - { - content: "Optimistic Vision", - duration: 15, - - }, - { - content: "Working Constitution", - duration: 20, - }, - { - content: "Delegate Expectations", - duration: 25, - }, - ] - }, - ] - + quests = Questing::QuestReadModel.all.map { |quest| quest_data(quest) } render json: quests end + def show - quest = { - img_url: "https://file.coinexstatic.com/2023-11-16/BB3FDB00283C55B4C36B94CFAC0C3271.png", - title: "Governance 101", - status: "start", - rewards: [ - { - type: "Points", - amount: 50 - }, - { - type: "OP", - amount: 2 - } - ], - intro: "First things first: understand what are the Optimism Values and what is expect of you in this important role.", - steps: [ - { - content: "Code of Conduct", - duration: 15, - }, - { - content: "Optimistic Vision", - duration: 10, - }, - { - content: "Working Constitution", - duration: 15, - }, - { - content: "Delegate Expectations", - duration: 12, - }, - ] + quest = Questing::QuestReadModel.find_by(quest_id: params[:id]) + if quest + render json: quest_data(quest) + else + render json: { error: "Quest not found" }, status: :not_found + end + end + + private + + def quest_data(quest) + { + id: quest.quest_id, + title: quest.title, + intro: quest.intro, + quest_type: quest.quest_type, + audience: quest.audience, + status: quest.status, + # TODO: rewards is a map, should be an array + rewards: [ quest.rewards ], + # add img_url + actions: fetch_quest_actions(quest.quest_id) } + end - render json: quest + def fetch_quest_actions(quest_id) + Questing::QuestActionReadModel.where(quest_id: quest_id) + .order(:position) + .includes(:action) + .map do |quest_action| + { + id: quest_action.action.action_id, + content: quest_action.action.content, + action_type: quest_action.action.action_type, + completion_criteria: quest_action.action.completion_criteria, + position: quest_action.position + } + end end -end \ No newline at end of file +end diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb index bdde1b9..fdc225b 100644 --- a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb @@ -1,12 +1,15 @@ module ActionTracking class OnActionCreated def call(event) - ActionReadModel.find_or_create_by(action_id: event.data[:action_id]).update( + action = ActionReadModel.create!( + action_id: event.data[:action_id], content: event.data[:content], - priority: event.data[:priority], - channel: event.data[:channel], - status: "Created" + action_type: event.data[:action_type], + completion_criteria: event.data[:completion_criteria] ) + Rails.logger.info "Action created in read model: #{action.action_id}" + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error "Failed to create action in read model: #{e.message}" end end end diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb index 9bb25fc..55095fc 100644 --- a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb @@ -1,3 +1,4 @@ +# rails_app/app/read_models/action_tracking/on_action_executed.rb module ActionTracking class OnActionExecuted def call(event) @@ -5,6 +6,7 @@ def call(event) action_id = event.data[:action_id] user_id = event.data[:user_id] timestamp = event.data[:timestamp] + completion_data = event.data[:completion_data] status = "Executed" ActionLogReadModel.create!( @@ -12,6 +14,7 @@ def call(event) action_id: action_id, user_id: user_id, executed_at: timestamp, + completion_data: completion_data, status: status ) end diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb index 9862f2f..8be2938 100644 --- a/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb @@ -5,9 +5,6 @@ class ActionReadModel < ApplicationRecord validates :action_id, presence: true, uniqueness: true validates :content, presence: true - validates :priority, presence: true - validates :channel, presence: true - validates :status, presence: true end class ActionLogReadModel < ApplicationRecord diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_action_associated_with_quest.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_action_associated_with_quest.rb new file mode 100644 index 0000000..c931ebd --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_action_associated_with_quest.rb @@ -0,0 +1,21 @@ +module Questing + class OnActionAssociatedWithQuest + def call(event) + quest = QuestReadModel.find_by(quest_id: event.data[:quest_id]) + action = ActionTracking::ActionReadModel.find_by(action_id: event.data[:action_id]) + + if quest && action + quest_action = QuestActionReadModel.create!( + quest: quest, + action: action, + position: event.data[:position] + ) + Rails.logger.info "Action associated with quest: Quest ID: #{quest.quest_id}, Action ID: #{action.action_id}" + else + Rails.logger.error "Failed to associate action with quest. Quest: #{event.data[:quest_id]}, Action: #{event.data[:action_id]}" + end + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error "Failed to create quest action association: #{e.message}" + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb index 9ec503c..b9c20e4 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb @@ -1,18 +1,18 @@ module Questing class OnQuestCreated def call(event) - QuestReadModel.find_or_initialize_by(quest_id: event.data[:quest_id]).tap do |quest| - quest.update!( - audience: event.data[:audience], - quest_type: event.data[:quest_type], - duration: event.data[:duration], - difficulty: event.data[:difficulty], - requirements: event.data[:requirements], - reward: event.data[:reward], - subquests: event.data[:subquests], - status: "created" - ) - end + quest = QuestReadModel.create!( + quest_id: event.data[:quest_id], + title: event.data[:title], + intro: event.data[:intro], + quest_type: event.data[:quest_type], + audience: event.data[:audience], + rewards: event.data[:reward], + status: "created" + ) + Rails.logger.info "Quest created in read model: #{quest.quest_id}" + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error "Failed to create quest in read model: #{e.message}" end end end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb index f35916d..9030577 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb @@ -1,6 +1,13 @@ module Questing class QuestReadModel < ApplicationRecord self.table_name = "quests" + has_many :quest_actions, class_name: "Questing::QuestActionReadModel", foreign_key: "quest_id" + end + + class QuestActionReadModel < ApplicationRecord + self.table_name = "quest_actions" + belongs_to :quest, class_name: "Questing::QuestReadModel", foreign_key: "quest_id", primary_key: "quest_id" + belongs_to :action, class_name: "ActionTracking::ActionReadModel", foreign_key: "action_id", primary_key: "action_id" end class UserQuest < ApplicationRecord @@ -10,7 +17,8 @@ class UserQuest < ApplicationRecord class ReadModelConfiguration def call(event_store) event_store.subscribe(OnQuestCreated, to: [ Questing::QuestCreated ]) - event_store.subscribe(OnUserStartedQuest, to: [ Questing::UserStartedQuest ]) + event_store.subscribe(OnActionAssociatedWithQuest, to: [ Questing::ActionAssociatedWithQuest ]) + # event_store.subscribe(OnUserStartedQuest, to: [Questing::UserStartedQuest]) end end end diff --git a/apps/govquests-api/rails_app/config/application.rb b/apps/govquests-api/rails_app/config/application.rb index 301ba8e..b3ceefc 100644 --- a/apps/govquests-api/rails_app/config/application.rb +++ b/apps/govquests-api/rails_app/config/application.rb @@ -40,5 +40,7 @@ class Application < Rails::Application # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true + + config.autoload_paths += %W[#{config.root}/app/read_models] end end diff --git a/apps/govquests-api/rails_app/config/initializers/cors.rb b/apps/govquests-api/rails_app/config/initializers/cors.rb index a0e3007..1cf822f 100644 --- a/apps/govquests-api/rails_app/config/initializers/cors.rb +++ b/apps/govquests-api/rails_app/config/initializers/cors.rb @@ -11,6 +11,6 @@ resource "*", headers: :any, - methods: [:get, :post, :put, :patch, :delete, :options, :head] + methods: [ :get, :post, :put, :patch, :delete, :options, :head ] end end diff --git a/apps/govquests-api/rails_app/config/routes.rb b/apps/govquests-api/rails_app/config/routes.rb index b0f97d1..deb6aa0 100644 --- a/apps/govquests-api/rails_app/config/routes.rb +++ b/apps/govquests-api/rails_app/config/routes.rb @@ -3,9 +3,12 @@ # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "rails/health#show", as: :rails_health_check + get "up" => "rails/health#show", :as => :rails_health_check # Defines the root path route ("/") # root "posts#index" - resources :quests + resources :quests, only: [ :index, :show ] + + post "/actions/:action_id/start", to: "action_completions#start" + post "/actions/:action_id/complete", to: "action_completions#complete" end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb index 378908d..b7a8881 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb @@ -2,14 +2,12 @@ class CreateQuests < ActiveRecord::Migration[8.0] def change create_table :quests do |t| t.string :quest_id, null: false, index: { unique: true } - t.string :audience, null: false + t.string :title, null: false + t.text :intro, null: false t.string :quest_type, null: false - t.integer :duration, null: false - t.string :difficulty, null: false - t.jsonb :requirements, default: [] - t.jsonb :reward, default: {} - t.jsonb :subquests, default: [] - t.string :status, default: "created" + t.string :audience, null: false + t.string :status, null: false + t.jsonb :rewards, null: false, default: {} t.timestamps end end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb b/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb index 42ea96b..972038d 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb @@ -4,9 +4,8 @@ def change create_table :actions do |t| t.string :action_id, null: false, index: { unique: true } t.string :content, null: false - t.string :priority, null: false - t.string :channel, null: false - t.string :status, default: "Pending" + t.string :action_type, null: false + t.jsonb :completion_criteria, null: false, default: {} t.timestamps end @@ -18,5 +17,15 @@ def change t.string :status, null: false t.timestamps end + + create_table :quest_actions do |t| + t.string :quest_id, null: false + t.string :action_id, null: false + t.integer :position, null: false + t.timestamps + end + + add_index :quest_actions, [ :quest_id, :action_id ], unique: true + add_index :quest_actions, [ :quest_id, :position ], unique: true end end diff --git a/apps/govquests-api/rails_app/db/migrate/20241001131854_create_quests.rb b/apps/govquests-api/rails_app/db/migrate/20241001131854_create_quests.rb deleted file mode 100644 index 21b52c7..0000000 --- a/apps/govquests-api/rails_app/db/migrate/20241001131854_create_quests.rb +++ /dev/null @@ -1,13 +0,0 @@ -class CreateQuests < ActiveRecord::Migration[8.0] - def change - create_table :quests do |t| - t.string :img_url - t.string :title - t.string :reward_type - t.text :intro - t.text :steps - - t.timestamps - end - end -end diff --git a/apps/govquests-api/rails_app/db/migrate/20241004125857_add_completion_data_to_action_logs.rb b/apps/govquests-api/rails_app/db/migrate/20241004125857_add_completion_data_to_action_logs.rb new file mode 100644 index 0000000..2a8cde5 --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20241004125857_add_completion_data_to_action_logs.rb @@ -0,0 +1,5 @@ +class AddCompletionDataToActionLogs < ActiveRecord::Migration[6.1] + def change + add_column :action_logs, :completion_data, :jsonb, default: {} + end +end diff --git a/apps/govquests-api/rails_app/db/schema.rb b/apps/govquests-api/rails_app/db/schema.rb index 62db998..6b0fba5 100644 --- a/apps/govquests-api/rails_app/db/schema.rb +++ b/apps/govquests-api/rails_app/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_09_30_201752) do +ActiveRecord::Schema[8.0].define(version: 2024_10_04_125857) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -22,15 +22,15 @@ t.string "status", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.jsonb "completion_data", default: {} t.index ["action_log_id"], name: "index_action_logs_on_action_log_id", unique: true end create_table "actions", force: :cascade do |t| t.string "action_id", null: false t.string "content", null: false - t.string "priority", null: false - t.string "channel", null: false - t.string "status", default: "Pending" + t.string "action_type", null: false + t.jsonb "completion_criteria", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["action_id"], name: "index_actions_on_action_id", unique: true @@ -110,16 +110,24 @@ t.index ["user_id"], name: "index_notifications_on_user_id" end + create_table "quest_actions", force: :cascade do |t| + t.string "quest_id", null: false + t.string "action_id", null: false + t.integer "position", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["quest_id", "action_id"], name: "index_quest_actions_on_quest_id_and_action_id", unique: true + t.index ["quest_id", "position"], name: "index_quest_actions_on_quest_id_and_position", unique: true + end + create_table "quests", force: :cascade do |t| t.string "quest_id", null: false - t.string "audience", null: false + t.string "title", null: false + t.text "intro", null: false t.string "quest_type", null: false - t.integer "duration", null: false - t.string "difficulty", null: false - t.jsonb "requirements", default: [] - t.jsonb "reward", default: {} - t.jsonb "subquests", default: [] - t.string "status", default: "created" + t.string "audience", null: false + t.string "status", null: false + t.jsonb "rewards", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["quest_id"], name: "index_quests_on_quest_id", unique: true diff --git a/apps/govquests-api/rails_app/db/seeds.rb b/apps/govquests-api/rails_app/db/seeds.rb index 3da3752..613c64b 100644 --- a/apps/govquests-api/rails_app/db/seeds.rb +++ b/apps/govquests-api/rails_app/db/seeds.rb @@ -1 +1,136 @@ -command_bus = Rails.configuration.command_bus +require "securerandom" + +def create_action(action_data) + action_id = SecureRandom.uuid + command = ActionTracking::CreateAction.new( + action_id: action_id, + content: action_data[:content], + action_type: action_data[:action_type], + completion_criteria: action_data[:completion_criteria] + ) + Rails.configuration.command_bus.call(command) + action_id +end + +actions_data = [ + { + content: "Read the Code of Conduct", + action_type: "ReadDocument", + completion_criteria: { document_url: "https://example.com/code-of-conduct" } + }, + { + content: "Watch the Optimistic Vision video", + action_type: "WatchVideo", + completion_criteria: { video_url: "https://example.com/optimistic-vision", minimum_watch_time: 540 } + }, + { + content: "Complete the Constitution Quiz", + action_type: "Quiz", + completion_criteria: { quiz_id: "const101", passing_score: 80 } + }, + { + content: "Submit your Delegate Statement", + action_type: "TextSubmission", + completion_criteria: { min_words: 200, max_words: 500 } + }, + { + content: "Participate in a Mock Proposal Vote", + action_type: "VotingSimulation", + completion_criteria: { proposal_id: "mock001", options: [ "Yes", "No", "Abstain" ] } + }, + { + content: "Analyze Governance Analytics Dashboard", + action_type: "DashboardInteraction", + completion_criteria: { dashboard_id: "gov_analytics_01", min_interaction_time: 1200 } + } +] + +puts "Creating actions..." +actions_data.each do |action_data| + action_id = create_action(action_data) + # rails_app/db/seeds/actions.rb (continued) + puts "Created action: #{action_data[:content]} (#{action_id})" +end + +puts "Actions created successfully!" + +quests_data = [ + { + title: "Governance 101", + intro: "Understand the Optimism Values and your role in governance.", + quest_type: "Onboarding", + audience: "AllUsers", + reward: { type: "Points", amount: 50 }, + actions: [ + "Read the Code of Conduct", + "Watch the Optimistic Vision video", + "Complete the Constitution Quiz", + "Submit your Delegate Statement" + ] + }, + { + title: "Advanced Governance Practices", + intro: "Dive deeper into governance processes and decision-making.", + quest_type: "Governance", + audience: "Delegates", + reward: { type: "Points", amount: 100 }, + actions: [ + "Participate in a Mock Proposal Vote", + "Analyze Governance Analytics Dashboard", + "Submit your Delegate Statement", + "Complete the Constitution Quiz" + ] + } +] + +def create_quest(quest_data) + quest_id = SecureRandom.uuid + command = Questing::CreateQuest.new( + quest_id: quest_id, + title: quest_data[:title], + intro: quest_data[:intro], + quest_type: quest_data[:quest_type], + audience: quest_data[:audience], + reward: quest_data[:reward] + ) + Rails.configuration.command_bus.call(command) + quest_id +end + +def associate_action_with_quest(quest_id, action_id, position) + command = Questing::AssociateActionWithQuest.new( + quest_id: quest_id, + action_id: action_id, + position: position + ) + Rails.configuration.command_bus.call(command) +end + +puts "Creating quests and associating actions..." +quests_data.each do |quest_data| + quest_id = create_quest(quest_data) + + # Wait for the quest to be created in the read model + retries = 0 + until Questing::QuestReadModel.exists?(quest_id: quest_id) + sleep(0.5) + retries += 1 + if retries > 10 + puts "Failed to create quest in read model after 5 seconds: #{quest_data[:title]}" + break + end + end + + quest_data[:actions].each_with_index do |action_content, index| + action = ActionTracking::ActionReadModel.find_by(content: action_content) + if action + associate_action_with_quest(quest_id, action.action_id, index + 1) + else + puts "Warning: Action '#{action_content}' not found" + end + end + + puts "Created quest: #{quest_data[:title]} (#{quest_id}) with #{quest_data[:actions].size} actions" +end + +puts "Quests created and actions associated successfully!" diff --git a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb index 1762bf1..883ce35 100644 --- a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb @@ -6,16 +6,16 @@ def setup @handler = OnActionCreated.new @action_id = SecureRandom.uuid @content = "Complete survey" - @priority = "High" - @channel = "Email" + @action_type = "ReadDocument" + @completion_criteria = { document_url: "https://example.com/survey" }.transform_keys(&:to_s) end test "creates a new action when handling ActionCreated event" do event = ActionCreated.new(data: { action_id: @action_id, content: @content, - priority: @priority, - channel: @channel + action_type: @action_type, + completion_criteria: @completion_criteria }) assert_difference "ActionReadModel.count", 1 do @@ -24,9 +24,8 @@ def setup action = ActionReadModel.find_by(action_id: @action_id) assert_equal @content, action.content - assert_equal @priority, action.priority - assert_equal @channel, action.channel - assert_equal "Created", action.status + assert_equal @action_type, action.action_type + assert_equal @completion_criteria, action.completion_criteria end end end diff --git a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb index 434f814..c9a5906 100644 --- a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb @@ -4,17 +4,18 @@ module ActionTracking class OnActionExecutedTest < ActiveSupport::TestCase def setup @handler = OnActionExecuted.new - @action_log_id = SecureRandom.uuid @action_id = SecureRandom.uuid @user_id = SecureRandom.uuid @timestamp = Time.current + @completion_data = { "result" => "success" } end test "creates a new action log when handling ActionExecuted event" do event = ActionExecuted.new(data: { action_id: @action_id, user_id: @user_id, - timestamp: @timestamp + timestamp: @timestamp, + completion_data: @completion_data }) assert_difference "ActionLogReadModel.count", 1 do diff --git a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_associated_with_action_test.rb b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_associated_with_action_test.rb new file mode 100644 index 0000000..cf63f42 --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_associated_with_action_test.rb @@ -0,0 +1,32 @@ +require "test_helper" + +module Questing + class OnActionAssociatedWithQuestTest < ActiveSupport::TestCase + def setup + @handler = Questing::OnActionAssociatedWithQuest.new + @quest_id = SecureRandom.uuid + @action_id = SecureRandom.uuid + @position = 1 + + QuestReadModel.create!(quest_id: @quest_id, title: "Test Quest", intro: "Test Intro", quest_type: "Test", audience: "All", status: "created") + ActionTracking::ActionReadModel.create!(action_id: @action_id, content: "Test Action", action_type: "Test", completion_criteria: {}) + end + + test "associates an action with a quest when handling ActionAssociatedWithQuest event" do + event = ActionAssociatedWithQuest.new(data: { + quest_id: @quest_id, + action_id: @action_id, + position: @position + }) + + assert_difference "QuestActionReadModel.count", 1 do + @handler.call(event) + end + + quest_action = QuestActionReadModel.last + assert_equal @quest_id, quest_action.quest_id + assert_equal @action_id, quest_action.action_id + assert_equal @position, quest_action.position + end + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb index c7f2a60..dd02b5c 100644 --- a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb @@ -1,3 +1,4 @@ +# rails_app/test/read_models/questing/on_quest_created_test.rb require "test_helper" module Questing @@ -5,25 +6,21 @@ class OnQuestCreatedTest < ActiveSupport::TestCase def setup @handler = Questing::OnQuestCreated.new @quest_id = SecureRandom.uuid + @title = "Governance 101" + @intro = "Learn about governance basics" + @quest_type = "Onboarding" @audience = "AllUsers" - @quest_type = "Standard" - @duration = 7 - @difficulty = "Medium" - @requirements = [ { "quest_type" => "action", "description" => "Complete action X" } ] - @reward = { "quest_type" => "points", "value" => 100 } - @subquests = [ { "id" => SecureRandom.uuid, "description" => "Subquest 1" } ] + @reward = { type: "Points", amount: 50 }.transform_keys(&:to_s) end test "creates a new quest when handling QuestCreated event" do event = QuestCreated.new(data: { quest_id: @quest_id, - audience: @audience, + title: @title, + intro: @intro, quest_type: @quest_type, - duration: @duration, - difficulty: @difficulty, - requirements: @requirements, - reward: @reward, - subquests: @subquests + audience: @audience, + reward: @reward }) assert_difference -> { QuestReadModel.count }, 1 do @@ -31,53 +28,12 @@ def setup end quest = QuestReadModel.find_by(quest_id: @quest_id) - assert_equal @audience, quest.audience + assert_equal @title, quest.title + assert_equal @intro, quest.intro assert_equal @quest_type, quest.quest_type - assert_equal @duration, quest.duration - assert_equal @difficulty, quest.difficulty - assert_equal @requirements, quest.requirements - assert_equal @reward, quest.reward - assert_equal @subquests, quest.subquests + assert_equal @audience, quest.audience + assert_equal @reward, quest.rewards assert_equal "created", quest.status end - - test "updates existing quest when handling QuestCreated event" do - existing_quest = QuestReadModel.create!( - quest_id: @quest_id, - audience: "Delegates", - quest_type: "Epic", - duration: 30, - difficulty: "Hard", - requirements: [], - reward: {}, - subquests: [], - status: "archived" - ) - - event = QuestCreated.new(data: { - quest_id: @quest_id, - audience: @audience, - quest_type: @quest_type, - duration: @duration, - difficulty: @difficulty, - requirements: @requirements, - reward: @reward, - subquests: @subquests - }) - - assert_no_difference -> { QuestReadModel.count } do - @handler.call(event) - end - - existing_quest.reload - assert_equal @audience, existing_quest.audience - assert_equal @quest_type, existing_quest.quest_type - assert_equal @duration, existing_quest.duration - assert_equal @difficulty, existing_quest.difficulty - assert_equal @requirements, existing_quest.requirements - assert_equal @reward, existing_quest.reward - assert_equal @subquests, existing_quest.subquests - assert_equal "created", existing_quest.status - end end end diff --git a/apps/govquests-api/rails_app/test/read_models/questing/on_user_started_quest_test.rb b/apps/govquests-api/rails_app/test/read_models/questing/on_user_started_quest_test.rb index 4eab2ac..74649ea 100644 --- a/apps/govquests-api/rails_app/test/read_models/questing/on_user_started_quest_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/questing/on_user_started_quest_test.rb @@ -1,26 +1,26 @@ -require "test_helper" +# require "test_helper" -module Questing - class OnUserStartedQuestTest < ActiveSupport::TestCase - def setup - @handler = Questing::OnUserStartedQuest.new - @quest_id = SecureRandom.uuid - @user_id = SecureRandom.uuid - end +# module Questing +# class OnUserStartedQuestTest < ActiveSupport::TestCase +# def setup +# @handler = Questing::OnUserStartedQuest.new +# @quest_id = SecureRandom.uuid +# @user_id = SecureRandom.uuid +# end - test "creates or updates user quest when handling UserStartedQuest event" do - event = Questing::UserStartedQuest.new(data: { - quest_id: @quest_id, - user_id: @user_id - }) +# test "creates or updates user quest when handling UserStartedQuest event" do +# event = Questing::UserStartedQuest.new(data: { +# quest_id: @quest_id, +# user_id: @user_id +# }) - assert_difference "UserQuest.count", 1 do - @handler.call(event) - end +# assert_difference "UserQuest.count", 1 do +# @handler.call(event) +# end - user_quest = UserQuest.find_by(quest_id: @quest_id, user_id: @user_id) - assert_equal "started", user_quest.status - assert_not_nil user_quest.started_at - end - end -end +# user_quest = UserQuest.find_by(quest_id: @quest_id, user_id: @user_id) +# assert_equal "started", user_quest.status +# assert_not_nil user_quest.started_at +# end +# end +# end diff --git a/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_created_test.rb b/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_created_test.rb index 485a68f..c050189 100644 --- a/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_created_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/rewarding/on_reward_created_test.rb @@ -1,3 +1,6 @@ +# rails_app/test/read_models/rewarding/on_reward_created_test.rb +require "test_helper" + module Rewarding class OnRewardCreatedTest < ActiveSupport::TestCase def setup From 81f6ef369fb7fde523d8d7448c63664c85184df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Fri, 4 Oct 2024 10:02:28 -0300 Subject: [PATCH 12/17] chore: add display_data jsonb column to quests,rewards,actions --- .../app/controllers/quests_controller.rb | 59 ++++++++----------- .../action_tracking/on_action_created.rb | 7 ++- .../read_model_configuration.rb | 9 ++- .../read_models/questing/on_quest_created.rb | 9 ++- .../questing/read_model_configuration.rb | 13 ++++ .../rewarding/read_model_configuration.rb | 9 ++- .../migrate/20240930201726_create_quests.rb | 3 +- .../migrate/20240930201744_create_rewards.rb | 1 + .../migrate/20240930201751_create_actions.rb | 3 +- ...5857_add_completion_data_to_action_logs.rb | 5 -- apps/govquests-api/rails_app/db/schema.rb | 10 ++-- apps/govquests-api/rails_app/db/seeds.rb | 18 +++--- 12 files changed, 82 insertions(+), 64 deletions(-) delete mode 100644 apps/govquests-api/rails_app/db/migrate/20241004125857_add_completion_data_to_action_logs.rb diff --git a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb index b20ae12..79c3fab 100644 --- a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb +++ b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb @@ -1,47 +1,38 @@ +# app/controllers/quests_controller.rb class QuestsController < ApplicationController def index - quests = Questing::QuestReadModel.all.map { |quest| quest_data(quest) } + quests = Questing::QuestReadModel.all.map do |quest| + { + id: quest.quest_id, + title: quest.title, + intro: quest.intro, + image_url: quest.image_url, + quest_type: quest.quest_type, + audience: quest.audience, + status: quest.status, + rewards: quest.rewards + } + end + render json: quests end def show quest = Questing::QuestReadModel.find_by(quest_id: params[:id]) + if quest - render json: quest_data(quest) + render json: { + id: quest.quest_id, + title: quest.title, + intro: quest.intro, + image_url: quest.image_url, + quest_type: quest.quest_type, + audience: quest.audience, + status: quest.status, + rewards: quest.rewards + } else render json: { error: "Quest not found" }, status: :not_found end end - - private - - def quest_data(quest) - { - id: quest.quest_id, - title: quest.title, - intro: quest.intro, - quest_type: quest.quest_type, - audience: quest.audience, - status: quest.status, - # TODO: rewards is a map, should be an array - rewards: [ quest.rewards ], - # add img_url - actions: fetch_quest_actions(quest.quest_id) - } - end - - def fetch_quest_actions(quest_id) - Questing::QuestActionReadModel.where(quest_id: quest_id) - .order(:position) - .includes(:action) - .map do |quest_action| - { - id: quest_action.action.action_id, - content: quest_action.action.content, - action_type: quest_action.action.action_type, - completion_criteria: quest_action.action.completion_criteria, - position: quest_action.position - } - end - end end diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb index fdc225b..f9cc1da 100644 --- a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb @@ -3,9 +3,12 @@ class OnActionCreated def call(event) action = ActionReadModel.create!( action_id: event.data[:action_id], - content: event.data[:content], action_type: event.data[:action_type], - completion_criteria: event.data[:completion_criteria] + completion_criteria: event.data[:completion_criteria], + display_data: { + content: event.data[:content], + duration: event.data[:duration] + } ) Rails.logger.info "Action created in read model: #{action.action_id}" rescue ActiveRecord::RecordInvalid => e diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb index 8be2938..37fac87 100644 --- a/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb @@ -4,7 +4,14 @@ class ActionReadModel < ApplicationRecord self.table_name = "actions" validates :action_id, presence: true, uniqueness: true - validates :content, presence: true + + def content + display_data["content"] + end + + def duration + display_data["duration"] + end end class ActionLogReadModel < ApplicationRecord diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb index b9c20e4..0105050 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb @@ -3,12 +3,15 @@ class OnQuestCreated def call(event) quest = QuestReadModel.create!( quest_id: event.data[:quest_id], - title: event.data[:title], - intro: event.data[:intro], quest_type: event.data[:quest_type], audience: event.data[:audience], rewards: event.data[:reward], - status: "created" + status: "created", + display_data: { + title: event.data[:title], + intro: event.data[:intro], + image_url: event.data[:image_url] + } ) Rails.logger.info "Quest created in read model: #{quest.quest_id}" rescue ActiveRecord::RecordInvalid => e diff --git a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb index 9030577..58fc7e6 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb @@ -1,7 +1,20 @@ module Questing class QuestReadModel < ApplicationRecord self.table_name = "quests" + has_many :quest_actions, class_name: "Questing::QuestActionReadModel", foreign_key: "quest_id" + + def title + display_data["title"] + end + + def intro + display_data["intro"] + end + + def image_url + display_data["image_url"] + end end class QuestActionReadModel < ApplicationRecord diff --git a/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb index c0d2aa4..db74095 100644 --- a/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb @@ -2,8 +2,13 @@ module Rewarding class RewardReadModel < ApplicationRecord self.table_name = "rewards" - validates :reward_id, presence: true, uniqueness: true - # Other validations... + def description + display_data["description"] + end + + def image_url + display_data["image_url"] + end end class ReadModelConfiguration diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb index b7a8881..87344ca 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb @@ -2,12 +2,11 @@ class CreateQuests < ActiveRecord::Migration[8.0] def change create_table :quests do |t| t.string :quest_id, null: false, index: { unique: true } - t.string :title, null: false - t.text :intro, null: false t.string :quest_type, null: false t.string :audience, null: false t.string :status, null: false t.jsonb :rewards, null: false, default: {} + t.jsonb :display_data, null: false, default: {} t.timestamps end end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb b/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb index 67d4042..295530a 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb @@ -8,6 +8,7 @@ def change t.string :issued_to t.string :delivery_status, default: "Pending" t.boolean :claimed, default: false + t.jsonb :display_data, null: false, default: {} t.timestamps end end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb b/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb index 972038d..0c58ef0 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb @@ -3,9 +3,9 @@ class CreateActions < ActiveRecord::Migration[8.0] def change create_table :actions do |t| t.string :action_id, null: false, index: { unique: true } - t.string :content, null: false t.string :action_type, null: false t.jsonb :completion_criteria, null: false, default: {} + t.jsonb :display_data, null: false, default: {} t.timestamps end @@ -15,6 +15,7 @@ def change t.string :user_id, null: false t.datetime :executed_at, null: false t.string :status, null: false + t.jsonb :completion_data, null: false, default: {} t.timestamps end diff --git a/apps/govquests-api/rails_app/db/migrate/20241004125857_add_completion_data_to_action_logs.rb b/apps/govquests-api/rails_app/db/migrate/20241004125857_add_completion_data_to_action_logs.rb deleted file mode 100644 index 2a8cde5..0000000 --- a/apps/govquests-api/rails_app/db/migrate/20241004125857_add_completion_data_to_action_logs.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddCompletionDataToActionLogs < ActiveRecord::Migration[6.1] - def change - add_column :action_logs, :completion_data, :jsonb, default: {} - end -end diff --git a/apps/govquests-api/rails_app/db/schema.rb b/apps/govquests-api/rails_app/db/schema.rb index 6b0fba5..98f8cd5 100644 --- a/apps/govquests-api/rails_app/db/schema.rb +++ b/apps/govquests-api/rails_app/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_10_04_125857) do +ActiveRecord::Schema[8.0].define(version: 2024_09_30_201752) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -20,17 +20,17 @@ t.string "user_id", null: false t.datetime "executed_at", null: false t.string "status", null: false + t.jsonb "completion_data", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.jsonb "completion_data", default: {} t.index ["action_log_id"], name: "index_action_logs_on_action_log_id", unique: true end create_table "actions", force: :cascade do |t| t.string "action_id", null: false - t.string "content", null: false t.string "action_type", null: false t.jsonb "completion_criteria", default: {}, null: false + t.jsonb "display_data", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["action_id"], name: "index_actions_on_action_id", unique: true @@ -122,12 +122,11 @@ create_table "quests", force: :cascade do |t| t.string "quest_id", null: false - t.string "title", null: false - t.text "intro", null: false t.string "quest_type", null: false t.string "audience", null: false t.string "status", null: false t.jsonb "rewards", default: {}, null: false + t.jsonb "display_data", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["quest_id"], name: "index_quests_on_quest_id", unique: true @@ -141,6 +140,7 @@ t.string "issued_to" t.string "delivery_status", default: "Pending" t.boolean "claimed", default: false + t.jsonb "display_data", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["reward_id"], name: "index_rewards_on_reward_id", unique: true diff --git a/apps/govquests-api/rails_app/db/seeds.rb b/apps/govquests-api/rails_app/db/seeds.rb index 613c64b..4142ccd 100644 --- a/apps/govquests-api/rails_app/db/seeds.rb +++ b/apps/govquests-api/rails_app/db/seeds.rb @@ -16,32 +16,32 @@ def create_action(action_data) { content: "Read the Code of Conduct", action_type: "ReadDocument", - completion_criteria: { document_url: "https://example.com/code-of-conduct" } + completion_criteria: {document_url: "https://example.com/code-of-conduct"} }, { content: "Watch the Optimistic Vision video", action_type: "WatchVideo", - completion_criteria: { video_url: "https://example.com/optimistic-vision", minimum_watch_time: 540 } + completion_criteria: {video_url: "https://example.com/optimistic-vision", minimum_watch_time: 540} }, { content: "Complete the Constitution Quiz", action_type: "Quiz", - completion_criteria: { quiz_id: "const101", passing_score: 80 } + completion_criteria: {quiz_id: "const101", passing_score: 80} }, { content: "Submit your Delegate Statement", action_type: "TextSubmission", - completion_criteria: { min_words: 200, max_words: 500 } + completion_criteria: {min_words: 200, max_words: 500} }, { content: "Participate in a Mock Proposal Vote", action_type: "VotingSimulation", - completion_criteria: { proposal_id: "mock001", options: [ "Yes", "No", "Abstain" ] } + completion_criteria: {proposal_id: "mock001", options: ["Yes", "No", "Abstain"]} }, { content: "Analyze Governance Analytics Dashboard", action_type: "DashboardInteraction", - completion_criteria: { dashboard_id: "gov_analytics_01", min_interaction_time: 1200 } + completion_criteria: {dashboard_id: "gov_analytics_01", min_interaction_time: 1200} } ] @@ -60,7 +60,7 @@ def create_action(action_data) intro: "Understand the Optimism Values and your role in governance.", quest_type: "Onboarding", audience: "AllUsers", - reward: { type: "Points", amount: 50 }, + reward: {type: "Points", amount: 50}, actions: [ "Read the Code of Conduct", "Watch the Optimistic Vision video", @@ -73,7 +73,7 @@ def create_action(action_data) intro: "Dive deeper into governance processes and decision-making.", quest_type: "Governance", audience: "Delegates", - reward: { type: "Points", amount: 100 }, + reward: {type: "Points", amount: 100}, actions: [ "Participate in a Mock Proposal Vote", "Analyze Governance Analytics Dashboard", @@ -122,7 +122,7 @@ def associate_action_with_quest(quest_id, action_id, position) end quest_data[:actions].each_with_index do |action_content, index| - action = ActionTracking::ActionReadModel.find_by(content: action_content) + action = ActionTracking::ActionReadModel.find_by("display_data @> ?", {content: action_content}.to_json) if action associate_action_with_quest(quest_id, action.action_id, index + 1) else From f998a965f2d3d26ec6e3ee5eff7bedebdd8aa64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Fri, 4 Oct 2024 10:13:00 -0300 Subject: [PATCH 13/17] chore: format/lint with standardrb --- .../govquests/authentication/Gemfile | 2 +- .../govquests/questing/test/test_helper.rb | 2 +- apps/govquests-api/infra/lib/infra.rb | 1 - .../lib/infra/aggregate_root_repository.rb | 4 +- apps/govquests-api/infra/lib/infra/event.rb | 2 +- apps/govquests-api/infra/lib/infra/process.rb | 10 ++-- apps/govquests-api/infra/lib/infra/testing.rb | 16 +++--- apps/govquests-api/infra/test/test_helper.rb | 1 - .../action_completions_controller.rb | 8 +-- .../app/controllers/quests_controller.rb | 2 +- .../read_model_configuration.rb | 4 +- .../authentication/on_user_registered.rb | 2 +- .../read_model_configuration.rb | 8 +-- .../gamification/on_badge_earned.rb | 2 +- .../gamification/read_model_configuration.rb | 10 ++-- .../notifications/on_notification_opened.rb | 2 +- .../notifications/on_notification_received.rb | 2 +- .../on_notification_scheduled.rb | 2 +- .../notifications/on_template_deleted.rb | 2 +- .../notifications/read_model_configuration.rb | 22 ++++---- .../on_action_associated_with_quest.rb | 2 +- .../questing/read_model_configuration.rb | 4 +- .../rewarding/read_model_configuration.rb | 10 ++-- .../read_models/single_table_read_model.rb | 4 +- apps/govquests-api/rails_app/bin/bundle | 4 +- apps/govquests-api/rails_app/bin/setup | 4 +- .../config/environments/development.rb | 4 +- .../config/environments/production.rb | 13 +++-- .../rails_app/config/environments/test.rb | 4 +- .../rails_app/config/initializers/cors.rb | 2 +- .../config/initializers/wrap_parameters.rb | 2 +- apps/govquests-api/rails_app/config/routes.rb | 2 +- ...0240926194656_create_event_store_events.rb | 24 ++++----- .../migrate/20240930201726_create_quests.rb | 2 +- .../20240930201733_create_notifications.rb | 6 +-- .../db/migrate/20240930201737_create_users.rb | 6 +-- .../migrate/20240930201744_create_rewards.rb | 2 +- .../migrate/20240930201751_create_actions.rb | 8 +-- ...0240930201752_create_user_game_profiles.rb | 4 +- .../rails_app/db/queue_schema.rb | 54 +++++++++---------- .../action_tracking/on_action_created_test.rb | 2 +- .../on_action_executed_test.rb | 2 +- .../authentication/on_user_registered_test.rb | 4 +- .../gamification/on_badge_earned_test.rb | 2 +- .../gamification/on_tier_achieved_test.rb | 4 +- .../questing/on_quest_created_test.rb | 2 +- .../rails_app/test/test_helper.rb | 12 ++--- 47 files changed, 144 insertions(+), 149 deletions(-) diff --git a/apps/govquests-api/govquests/authentication/Gemfile b/apps/govquests-api/govquests/authentication/Gemfile index 8fd8871..00fef6d 100644 --- a/apps/govquests-api/govquests/authentication/Gemfile +++ b/apps/govquests-api/govquests/authentication/Gemfile @@ -1,4 +1,4 @@ source "https://rubygems.org" eval_gemfile "../../infra/Gemfile.test" -gem "infra", path: "../../infra" \ No newline at end of file +gem "infra", path: "../../infra" diff --git a/apps/govquests-api/govquests/questing/test/test_helper.rb b/apps/govquests-api/govquests/questing/test/test_helper.rb index e77a85b..ef87e15 100644 --- a/apps/govquests-api/govquests/questing/test/test_helper.rb +++ b/apps/govquests-api/govquests/questing/test/test_helper.rb @@ -6,7 +6,7 @@ module Questing class Test < Infra::InMemoryTest def before_setup - super() + super Configuration.new.call(event_store, command_bus) end diff --git a/apps/govquests-api/infra/lib/infra.rb b/apps/govquests-api/infra/lib/infra.rb index 8e9d950..af40a31 100644 --- a/apps/govquests-api/infra/lib/infra.rb +++ b/apps/govquests-api/infra/lib/infra.rb @@ -3,7 +3,6 @@ require "arkency/command_bus" require "dry-struct" require "dry-types" -require "aggregate_root" require "active_support/notifications" require "minitest" require "ruby_event_store/transformations" diff --git a/apps/govquests-api/infra/lib/infra/aggregate_root_repository.rb b/apps/govquests-api/infra/lib/infra/aggregate_root_repository.rb index e338325..18de4dd 100644 --- a/apps/govquests-api/infra/lib/infra/aggregate_root_repository.rb +++ b/apps/govquests-api/infra/lib/infra/aggregate_root_repository.rb @@ -11,11 +11,11 @@ def initialize(event_store, notifications = ActiveSupport::Notifications) ) end - def with_aggregate(aggregate_class, aggregate_id, &block) + def with_aggregate(aggregate_class, aggregate_id, &) @repository.with_aggregate( aggregate_class.new(aggregate_id), stream_name(aggregate_class, aggregate_id), - &block + & ) end diff --git a/apps/govquests-api/infra/lib/infra/event.rb b/apps/govquests-api/infra/lib/infra/event.rb index 67a1017..4d82c00 100644 --- a/apps/govquests-api/infra/lib/infra/event.rb +++ b/apps/govquests-api/infra/lib/infra/event.rb @@ -23,7 +23,7 @@ def initialize(event_id: SecureRandom.uuid, metadata: nil, data: {}) end def self.included(klass) - klass.extend WithSchema::ClassMethods + klass.extend WithSchema::ClassMethods klass.include WithSchema::Constructor end end diff --git a/apps/govquests-api/infra/lib/infra/process.rb b/apps/govquests-api/infra/lib/infra/process.rb index 6958ee1..31ea7cd 100644 --- a/apps/govquests-api/infra/lib/infra/process.rb +++ b/apps/govquests-api/infra/lib/infra/process.rb @@ -10,11 +10,9 @@ def call(event_name, event_data_keys, command, command_data_keys) ->(event) do @command_bus.call( command.new( - Hash[ - command_data_keys.zip( - event_data_keys.map { |key| event.data.fetch(key) } - ) - ] + command_data_keys.zip( + event_data_keys.map { |key| event.data.fetch(key) } + ).to_h ) ) end, @@ -22,4 +20,4 @@ def call(event_name, event_data_keys, command, command_data_keys) ) end end -end \ No newline at end of file +end diff --git a/apps/govquests-api/infra/lib/infra/testing.rb b/apps/govquests-api/infra/lib/infra/testing.rb index ee77783..294ccd7 100644 --- a/apps/govquests-api/infra/lib/infra/testing.rb +++ b/apps/govquests-api/infra/lib/infra/testing.rb @@ -14,9 +14,9 @@ def self.with(event_store:, command_bus:) def self.included(klass) klass.include TestPlumbing.with( - event_store: -> { EventStore.in_memory }, - command_bus: -> { CommandBus.new } - ) + event_store: -> { EventStore.in_memory }, + command_bus: -> { CommandBus.new } + ) end module TestMethods @@ -27,9 +27,9 @@ def arrange(*commands) end def act(command) - command_bus.(command) + command_bus.call(command) end - alias run_command act + alias_method :run_command, :act def assert_events(stream_name, *expected_events) scope = event_store.read.stream(stream_name) @@ -37,9 +37,9 @@ def assert_events(stream_name, *expected_events) yield actual_events = before.nil? ? scope.to_a : scope.from(before.event_id).to_a - to_compare = ->(ev) { { type: ev.event_type, data: ev.data } } + to_compare = ->(ev) { {type: ev.event_type, data: ev.data} } assert_equal expected_events.map(&to_compare), - actual_events.map(&to_compare) + actual_events.map(&to_compare) end def assert_events_contain(stream_name, *expected_events) @@ -48,7 +48,7 @@ def assert_events_contain(stream_name, *expected_events) yield actual_events = before.nil? ? scope.to_a : scope.from(before.event_id).to_a - to_compare = ->(ev) { { type: ev.event_type, data: ev.data } } + to_compare = ->(ev) { {type: ev.event_type, data: ev.data} } expected_events.map(&to_compare).each do |expected| assert_includes(actual_events.map(&to_compare), expected) end diff --git a/apps/govquests-api/infra/test/test_helper.rb b/apps/govquests-api/infra/test/test_helper.rb index ddea463..5668153 100644 --- a/apps/govquests-api/infra/test/test_helper.rb +++ b/apps/govquests-api/infra/test/test_helper.rb @@ -2,4 +2,3 @@ require "mutant/minitest/coverage" require_relative "../lib/infra" - diff --git a/apps/govquests-api/rails_app/app/controllers/action_completions_controller.rb b/apps/govquests-api/rails_app/app/controllers/action_completions_controller.rb index 3aac577..fb6d08b 100644 --- a/apps/govquests-api/rails_app/app/controllers/action_completions_controller.rb +++ b/apps/govquests-api/rails_app/app/controllers/action_completions_controller.rb @@ -3,9 +3,9 @@ def start action = ActionTracking::ActionReadModel.find_by(action_id: params[:action_id]) if action token = generate_completion_token(action.action_id) - render json: { token: token, expires_at: 30.minutes.from_now } + render json: {token: token, expires_at: 30.minutes.from_now} else - render json: { error: "Action not found" }, status: :not_found + render json: {error: "Action not found"}, status: :not_found end end @@ -18,9 +18,9 @@ def complete completion_data: params[:completion_data] ) Rails.configuration.command_bus.call(command) - render json: { message: "Action completed successfully" } + render json: {message: "Action completed successfully"} else - render json: { error: "Invalid completion attempt" }, status: :unprocessable_entity + render json: {error: "Invalid completion attempt"}, status: :unprocessable_entity end end diff --git a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb index 79c3fab..a9fd733 100644 --- a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb +++ b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb @@ -32,7 +32,7 @@ def show rewards: quest.rewards } else - render json: { error: "Quest not found" }, status: :not_found + render json: {error: "Quest not found"}, status: :not_found end end end diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb index 37fac87..35820ff 100644 --- a/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/read_model_configuration.rb @@ -26,8 +26,8 @@ class ActionLogReadModel < ApplicationRecord class ReadModelConfiguration def call(event_store) - event_store.subscribe(OnActionCreated, to: [ ActionTracking::ActionCreated ]) - event_store.subscribe(OnActionExecuted, to: [ ActionTracking::ActionExecuted ]) + event_store.subscribe(OnActionCreated, to: [ActionTracking::ActionCreated]) + event_store.subscribe(OnActionExecuted, to: [ActionTracking::ActionExecuted]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb index cefa1e7..20d17be 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/on_user_registered.rb @@ -10,7 +10,7 @@ def call(event) user = UserReadModel.find_or_initialize_by(user_id: user_id) user.email = email user.user_type = user_type - user.wallets = [ { wallet_address: wallet_address, chain_id: chain_id } ] + user.wallets = [{wallet_address: wallet_address, chain_id: chain_id}] user.save! end end diff --git a/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb index c614fcc..12f538c 100644 --- a/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/authentication/read_model_configuration.rb @@ -3,8 +3,8 @@ class UserReadModel < ApplicationRecord self.table_name = "users" validates :user_id, presence: true, uniqueness: true - validates :email, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } - validates :user_type, presence: true, inclusion: { in: %w[delegate non_delegate] } + validates :email, uniqueness: true, format: {with: URI::MailTo::EMAIL_REGEXP} + validates :user_type, presence: true, inclusion: {in: %w[delegate non_delegate]} end class SessionReadModel < ApplicationRecord @@ -18,8 +18,8 @@ class SessionReadModel < ApplicationRecord class ReadModelConfiguration def call(event_store) - event_store.subscribe(OnUserRegistered.new, to: [ Authentication::UserRegistered ]) - event_store.subscribe(OnUserLoggedIn.new, to: [ Authentication::UserLoggedIn ]) + event_store.subscribe(OnUserRegistered.new, to: [Authentication::UserRegistered]) + event_store.subscribe(OnUserLoggedIn.new, to: [Authentication::UserLoggedIn]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb b/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb index cf8b2da..64d6f44 100644 --- a/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb +++ b/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb @@ -10,7 +10,7 @@ def call(event) if badges.include?(badge) Rails.logger.info "Badge '#{badge}' already exists for GameProfile #{profile_id}" else - game_profile.update(badges: badges + [ badge ]) + game_profile.update(badges: badges + [badge]) Rails.logger.info "Added badge '#{badge}' to GameProfile #{profile_id}" end else diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/gamification/read_model_configuration.rb index b22a1c6..4908070 100644 --- a/apps/govquests-api/rails_app/app/read_models/gamification/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/gamification/read_model_configuration.rb @@ -34,11 +34,11 @@ class LeaderboardEntryReadModel < ApplicationRecord class ReadModelConfiguration def call(event_store) - event_store.subscribe(OnTierAchieved, to: [ Gamification::TierAchieved ]) - event_store.subscribe(OnTrackCompleted, to: [ Gamification::TrackCompleted ]) - event_store.subscribe(OnStreakMaintained, to: [ Gamification::StreakMaintained ]) - event_store.subscribe(OnBadgeEarned, to: [ Gamification::BadgeEarned ]) - event_store.subscribe(OnLeaderboardUpdated, to: [ Gamification::LeaderboardUpdated ]) + event_store.subscribe(OnTierAchieved, to: [Gamification::TierAchieved]) + event_store.subscribe(OnTrackCompleted, to: [Gamification::TrackCompleted]) + event_store.subscribe(OnStreakMaintained, to: [Gamification::StreakMaintained]) + event_store.subscribe(OnBadgeEarned, to: [Gamification::BadgeEarned]) + event_store.subscribe(OnLeaderboardUpdated, to: [Gamification::LeaderboardUpdated]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_opened.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_opened.rb index 2529991..a7e9895 100644 --- a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_opened.rb +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_opened.rb @@ -5,7 +5,7 @@ def call(event) opened_at = event.data.fetch(:opened_at) notification = NotificationReadModel.find_by(notification_id: notification_id) - notification.update(opened_at: opened_at, status: "opened") if notification + notification&.update(opened_at: opened_at, status: "opened") end end end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_received.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_received.rb index 727c8f7..0b50f75 100644 --- a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_received.rb +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_received.rb @@ -5,7 +5,7 @@ def call(event) received_at = event.data.fetch(:received_at) notification = NotificationReadModel.find_by(notification_id: notification_id) - notification.update(received_at: received_at, status: "received") if notification + notification&.update(received_at: received_at, status: "received") end end end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_scheduled.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_scheduled.rb index 37285c0..a967775 100644 --- a/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_scheduled.rb +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_notification_scheduled.rb @@ -5,7 +5,7 @@ def call(event) scheduled_time = event.data.fetch(:scheduled_time) notification = NotificationReadModel.find_by(notification_id: notification_id) - notification.update(scheduled_time: scheduled_time, status: "scheduled") if notification + notification&.update(scheduled_time: scheduled_time, status: "scheduled") end end end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/on_template_deleted.rb b/apps/govquests-api/rails_app/app/read_models/notifications/on_template_deleted.rb index 87047b2..2afb062 100644 --- a/apps/govquests-api/rails_app/app/read_models/notifications/on_template_deleted.rb +++ b/apps/govquests-api/rails_app/app/read_models/notifications/on_template_deleted.rb @@ -4,7 +4,7 @@ def call(event) template_id = event.data.fetch(:template_id) template = NotificationTemplateReadModel.find_by(template_id: template_id) - template.destroy if template + template&.destroy end end end diff --git a/apps/govquests-api/rails_app/app/read_models/notifications/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/notifications/read_model_configuration.rb index bc8d084..b5564b9 100644 --- a/apps/govquests-api/rails_app/app/read_models/notifications/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/notifications/read_model_configuration.rb @@ -6,8 +6,8 @@ class NotificationReadModel < ApplicationRecord validates :template_id, presence: true validates :user_id, presence: true validates :channel, presence: true - validates :priority, presence: true, numericality: { only_integer: true, greater_than: 0, less_than: 6 } - validates :status, presence: true, inclusion: { in: %w[created scheduled sent received opened] } + validates :priority, presence: true, numericality: {only_integer: true, greater_than: 0, less_than: 6} + validates :status, presence: true, inclusion: {in: %w[created scheduled sent received opened]} end class NotificationTemplateReadModel < ApplicationRecord @@ -16,22 +16,22 @@ class NotificationTemplateReadModel < ApplicationRecord validates :template_id, presence: true, uniqueness: true validates :name, presence: true, uniqueness: true validates :content, presence: true - validates :template_type, presence: true, inclusion: { in: [ "email", "SMS", "push" ] } + validates :template_type, presence: true, inclusion: {in: ["email", "SMS", "push"]} end class ReadModelConfiguration def call(event_store) # Notification Events - event_store.subscribe(OnNotificationCreated, to: [ Notifications::NotificationCreated ]) - event_store.subscribe(OnNotificationScheduled, to: [ Notifications::NotificationScheduled ]) - event_store.subscribe(OnNotificationSent, to: [ Notifications::NotificationSent ]) - event_store.subscribe(OnNotificationReceived, to: [ Notifications::NotificationReceived ]) - event_store.subscribe(OnNotificationOpened, to: [ Notifications::NotificationOpened ]) + event_store.subscribe(OnNotificationCreated, to: [Notifications::NotificationCreated]) + event_store.subscribe(OnNotificationScheduled, to: [Notifications::NotificationScheduled]) + event_store.subscribe(OnNotificationSent, to: [Notifications::NotificationSent]) + event_store.subscribe(OnNotificationReceived, to: [Notifications::NotificationReceived]) + event_store.subscribe(OnNotificationOpened, to: [Notifications::NotificationOpened]) # NotificationTemplate Events - event_store.subscribe(OnTemplateCreated, to: [ Notifications::NotificationTemplateCreated ]) - event_store.subscribe(OnTemplateUpdated, to: [ Notifications::NotificationTemplateUpdated ]) - event_store.subscribe(OnTemplateDeleted, to: [ Notifications::NotificationTemplateDeleted ]) + event_store.subscribe(OnTemplateCreated, to: [Notifications::NotificationTemplateCreated]) + event_store.subscribe(OnTemplateUpdated, to: [Notifications::NotificationTemplateUpdated]) + event_store.subscribe(OnTemplateDeleted, to: [Notifications::NotificationTemplateDeleted]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_action_associated_with_quest.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_action_associated_with_quest.rb index c931ebd..e27bc99 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/on_action_associated_with_quest.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_action_associated_with_quest.rb @@ -5,7 +5,7 @@ def call(event) action = ActionTracking::ActionReadModel.find_by(action_id: event.data[:action_id]) if quest && action - quest_action = QuestActionReadModel.create!( + QuestActionReadModel.create!( quest: quest, action: action, position: event.data[:position] diff --git a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb index 58fc7e6..41d5055 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb @@ -29,8 +29,8 @@ class UserQuest < ApplicationRecord class ReadModelConfiguration def call(event_store) - event_store.subscribe(OnQuestCreated, to: [ Questing::QuestCreated ]) - event_store.subscribe(OnActionAssociatedWithQuest, to: [ Questing::ActionAssociatedWithQuest ]) + event_store.subscribe(OnQuestCreated, to: [Questing::QuestCreated]) + event_store.subscribe(OnActionAssociatedWithQuest, to: [Questing::ActionAssociatedWithQuest]) # event_store.subscribe(OnUserStartedQuest, to: [Questing::UserStartedQuest]) end end diff --git a/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb index db74095..c0ae878 100644 --- a/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/rewarding/read_model_configuration.rb @@ -13,11 +13,11 @@ def image_url class ReadModelConfiguration def call(event_store) - event_store.subscribe(OnRewardCreated, to: [ Rewarding::RewardCreated ]) - event_store.subscribe(OnRewardIssued, to: [ Rewarding::RewardIssued ]) - event_store.subscribe(OnRewardClaimed, to: [ Rewarding::RewardClaimed ]) - event_store.subscribe(OnRewardExpired, to: [ Rewarding::RewardExpired ]) - event_store.subscribe(OnRewardInventoryDepleted, to: [ Rewarding::RewardInventoryDepleted ]) + event_store.subscribe(OnRewardCreated, to: [Rewarding::RewardCreated]) + event_store.subscribe(OnRewardIssued, to: [Rewarding::RewardIssued]) + event_store.subscribe(OnRewardClaimed, to: [Rewarding::RewardClaimed]) + event_store.subscribe(OnRewardExpired, to: [Rewarding::RewardExpired]) + event_store.subscribe(OnRewardInventoryDepleted, to: [Rewarding::RewardInventoryDepleted]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/single_table_read_model.rb b/apps/govquests-api/rails_app/app/read_models/single_table_read_model.rb index 3a7d080..93501e2 100644 --- a/apps/govquests-api/rails_app/app/read_models/single_table_read_model.rb +++ b/apps/govquests-api/rails_app/app/read_models/single_table_read_model.rb @@ -6,11 +6,11 @@ def initialize(event_store, active_record_name, id_column) end def subscribe_create(creation_event) - @event_store.subscribe(create_handler(creation_event), to: [ creation_event ]) + @event_store.subscribe(create_handler(creation_event), to: [creation_event]) end def subscribe_copy(event, sequence_of_keys, column = Array(sequence_of_keys).join("_")) - @event_store.subscribe(copy_handler(event, sequence_of_keys, column), to: [ event ]) + @event_store.subscribe(copy_handler(event, sequence_of_keys, column), to: [event]) end private diff --git a/apps/govquests-api/rails_app/bin/bundle b/apps/govquests-api/rails_app/bin/bundle index 50da5fd..a0f692a 100755 --- a/apps/govquests-api/rails_app/bin/bundle +++ b/apps/govquests-api/rails_app/bin/bundle @@ -30,7 +30,7 @@ m = Module.new do if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) bundler_version = a end - next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/o bundler_version = $1 update_index = i end @@ -56,7 +56,7 @@ m = Module.new do def lockfile_version return unless File.file?(lockfile) lockfile_contents = File.read(lockfile) - return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/o Regexp.last_match(1) end diff --git a/apps/govquests-api/rails_app/bin/setup b/apps/govquests-api/rails_app/bin/setup index 3cd5a9d..3ec5486 100755 --- a/apps/govquests-api/rails_app/bin/setup +++ b/apps/govquests-api/rails_app/bin/setup @@ -4,8 +4,8 @@ require "fileutils" # path to your application root. APP_ROOT = File.expand_path("..", __dir__) -def system!(*args) - system(*args, exception: true) +def system!(*) + system(*, exception: true) end FileUtils.chdir APP_ROOT do diff --git a/apps/govquests-api/rails_app/config/environments/development.rb b/apps/govquests-api/rails_app/config/environments/development.rb index 8526e90..a49a790 100644 --- a/apps/govquests-api/rails_app/config/environments/development.rb +++ b/apps/govquests-api/rails_app/config/environments/development.rb @@ -18,7 +18,7 @@ # Enable/disable Action Controller caching. By default Action Controller caching is disabled. # Run rails dev:cache to toggle Action Controller caching. if Rails.root.join("tmp/caching-dev.txt").exist? - config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + config.public_file_server.headers = {"cache-control" => "public, max-age=#{2.days.to_i}"} else config.action_controller.perform_caching = false end @@ -36,7 +36,7 @@ config.action_mailer.perform_caching = false # Set localhost to be used by links generated in mailer templates. - config.action_mailer.default_url_options = { host: "localhost", port: 3001 } + config.action_mailer.default_url_options = {host: "localhost", port: 3001} # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log diff --git a/apps/govquests-api/rails_app/config/environments/production.rb b/apps/govquests-api/rails_app/config/environments/production.rb index 967ae67..c864b03 100644 --- a/apps/govquests-api/rails_app/config/environments/production.rb +++ b/apps/govquests-api/rails_app/config/environments/production.rb @@ -13,7 +13,7 @@ config.consider_all_requests_local = false # Cache assets for far-future expiry since they are all digest stamped. - config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + config.public_file_server.headers = {"cache-control" => "public, max-age=#{1.year.to_i}"} # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" @@ -31,8 +31,8 @@ # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } # Log to STDOUT with the current request id as a default log tag. - config.log_tags = [ :request_id ] - config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + config.log_tags = [:request_id] + config.logger = ActiveSupport::TaggedLogging.logger($stdout) # Change to "debug" to log everything (including potentially personally-identifiable information!) config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") @@ -48,15 +48,14 @@ # Replace the default in-process and non-durable queuing backend for Active Job. config.active_job.queue_adapter = :solid_queue - config.solid_queue.connects_to = { database: { writing: :queue } } - + config.solid_queue.connects_to = {database: {writing: :queue}} # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = { host: "example.com" } + config.action_mailer.default_url_options = {host: "example.com"} # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. # config.action_mailer.smtp_settings = { @@ -75,7 +74,7 @@ config.active_record.dump_schema_after_migration = false # Only use :id for inspections in production. - config.active_record.attributes_for_inspect = [ :id ] + config.active_record.attributes_for_inspect = [:id] # Enable DNS rebinding protection and other `Host` header attacks. # config.hosts = [ diff --git a/apps/govquests-api/rails_app/config/environments/test.rb b/apps/govquests-api/rails_app/config/environments/test.rb index c2095b1..0fb6d30 100644 --- a/apps/govquests-api/rails_app/config/environments/test.rb +++ b/apps/govquests-api/rails_app/config/environments/test.rb @@ -16,7 +16,7 @@ config.eager_load = ENV["CI"].present? # Configure public file server for tests with cache-control for performance. - config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + config.public_file_server.headers = {"cache-control" => "public, max-age=3600"} # Show full error reports. config.consider_all_requests_local = true @@ -37,7 +37,7 @@ config.action_mailer.delivery_method = :test # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = { host: "example.com" } + config.action_mailer.default_url_options = {host: "example.com"} # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr diff --git a/apps/govquests-api/rails_app/config/initializers/cors.rb b/apps/govquests-api/rails_app/config/initializers/cors.rb index 1cf822f..a0e3007 100644 --- a/apps/govquests-api/rails_app/config/initializers/cors.rb +++ b/apps/govquests-api/rails_app/config/initializers/cors.rb @@ -11,6 +11,6 @@ resource "*", headers: :any, - methods: [ :get, :post, :put, :patch, :delete, :options, :head ] + methods: [:get, :post, :put, :patch, :delete, :options, :head] end end diff --git a/apps/govquests-api/rails_app/config/initializers/wrap_parameters.rb b/apps/govquests-api/rails_app/config/initializers/wrap_parameters.rb index 6d55fe7..bbfc396 100644 --- a/apps/govquests-api/rails_app/config/initializers/wrap_parameters.rb +++ b/apps/govquests-api/rails_app/config/initializers/wrap_parameters.rb @@ -5,7 +5,7 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [ :json ] + wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. diff --git a/apps/govquests-api/rails_app/config/routes.rb b/apps/govquests-api/rails_app/config/routes.rb index deb6aa0..2358871 100644 --- a/apps/govquests-api/rails_app/config/routes.rb +++ b/apps/govquests-api/rails_app/config/routes.rb @@ -7,7 +7,7 @@ # Defines the root path route ("/") # root "posts#index" - resources :quests, only: [ :index, :show ] + resources :quests, only: [:index, :show] post "/actions/:action_id/start", to: "action_completions#start" post "/actions/:action_id/complete", to: "action_completions#complete" diff --git a/apps/govquests-api/rails_app/db/migrate/20240926194656_create_event_store_events.rb b/apps/govquests-api/rails_app/db/migrate/20240926194656_create_event_store_events.rb index 7d5b518..2776616 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240926194656_create_event_store_events.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240926194656_create_event_store_events.rb @@ -3,22 +3,22 @@ class CreateEventStoreEvents < ActiveRecord::Migration[8.0] def change create_table(:event_store_events, force: false) do |t| - t.references :event, null: false, type: :string, limit: 36, index: { unique: true } - t.string :event_type, null: false, index: true - t.binary :metadata - t.binary :data, null: false - t.datetime :created_at, null: false, precision: 6, index: true - t.datetime :valid_at, null: true, precision: 6, index: true + t.references :event, null: false, type: :string, limit: 36, index: {unique: true} + t.string :event_type, null: false, index: true + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false, precision: 6, index: true + t.datetime :valid_at, null: true, precision: 6, index: true end create_table(:event_store_events_in_streams, force: false) do |t| - t.string :stream, null: false - t.integer :position, null: true, default: :null - t.references :event, null: false, type: :string, limit: 36, index: true, foreign_key: { to_table: :event_store_events, primary_key: :event_id } - t.datetime :created_at, null: false, precision: 6, index: true + t.string :stream, null: false + t.integer :position, null: true, default: :null + t.references :event, null: false, type: :string, limit: 36, index: true, foreign_key: {to_table: :event_store_events, primary_key: :event_id} + t.datetime :created_at, null: false, precision: 6, index: true - t.index [ :stream, :position ], unique: true - t.index [ :stream, :event_id ], unique: true + t.index [:stream, :position], unique: true + t.index [:stream, :event_id], unique: true end end end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb index 87344ca..c6a307f 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201726_create_quests.rb @@ -1,7 +1,7 @@ class CreateQuests < ActiveRecord::Migration[8.0] def change create_table :quests do |t| - t.string :quest_id, null: false, index: { unique: true } + t.string :quest_id, null: false, index: {unique: true} t.string :quest_type, null: false t.string :audience, null: false t.string :status, null: false diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb b/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb index 0eaf553..8b4878d 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201733_create_notifications.rb @@ -1,7 +1,7 @@ class CreateNotifications < ActiveRecord::Migration[8.0] def change create_table :notifications do |t| - t.string :notification_id, null: false, index: { unique: true } + t.string :notification_id, null: false, index: {unique: true} t.string :content, null: false t.integer :priority, null: false t.string :template_id, null: false @@ -17,8 +17,8 @@ def change end create_table :notification_templates do |t| - t.string :template_id, null: false, index: { unique: true } - t.string :name, null: false, index: { unique: true } + t.string :template_id, null: false, index: {unique: true} + t.string :name, null: false, index: {unique: true} t.text :content, null: false t.string :template_type, null: false t.timestamps diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb b/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb index afbdaf6..a8588ef 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201737_create_users.rb @@ -1,8 +1,8 @@ class CreateUsers < ActiveRecord::Migration[8.0] def change create_table :users do |t| - t.string :user_id, null: false, index: { unique: true } - t.string :email, null: false, index: { unique: true } + t.string :user_id, null: false, index: {unique: true} + t.string :email, null: false, index: {unique: true} t.string :user_type, null: false, default: "non_delegate" t.jsonb :settings, default: {} t.jsonb :wallets, default: [] @@ -28,6 +28,6 @@ def change end add_index :user_sessions, :user_id - add_index :user_rewards, [ :user_id, :reward_id ], unique: true + add_index :user_rewards, [:user_id, :reward_id], unique: true end end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb b/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb index 295530a..e66da46 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201744_create_rewards.rb @@ -1,7 +1,7 @@ class CreateRewards < ActiveRecord::Migration[8.0] def change create_table :rewards do |t| - t.string :reward_id, null: false, index: { unique: true } + t.string :reward_id, null: false, index: {unique: true} t.string :reward_type, null: false t.integer :value, null: false t.datetime :expiry_date diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb b/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb index 0c58ef0..43a0ed4 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201751_create_actions.rb @@ -2,7 +2,7 @@ class CreateActions < ActiveRecord::Migration[8.0] def change create_table :actions do |t| - t.string :action_id, null: false, index: { unique: true } + t.string :action_id, null: false, index: {unique: true} t.string :action_type, null: false t.jsonb :completion_criteria, null: false, default: {} t.jsonb :display_data, null: false, default: {} @@ -10,7 +10,7 @@ def change end create_table :action_logs do |t| - t.string :action_log_id, null: false, index: { unique: true } + t.string :action_log_id, null: false, index: {unique: true} t.string :action_id, null: false t.string :user_id, null: false t.datetime :executed_at, null: false @@ -26,7 +26,7 @@ def change t.timestamps end - add_index :quest_actions, [ :quest_id, :action_id ], unique: true - add_index :quest_actions, [ :quest_id, :position ], unique: true + add_index :quest_actions, [:quest_id, :action_id], unique: true + add_index :quest_actions, [:quest_id, :position], unique: true end end diff --git a/apps/govquests-api/rails_app/db/migrate/20240930201752_create_user_game_profiles.rb b/apps/govquests-api/rails_app/db/migrate/20240930201752_create_user_game_profiles.rb index 1a0313e..2b98693 100644 --- a/apps/govquests-api/rails_app/db/migrate/20240930201752_create_user_game_profiles.rb +++ b/apps/govquests-api/rails_app/db/migrate/20240930201752_create_user_game_profiles.rb @@ -1,7 +1,7 @@ class CreateUserGameProfiles < ActiveRecord::Migration[8.0] def change create_table :user_game_profiles do |t| - t.string :profile_id, null: false, index: { unique: true } + t.string :profile_id, null: false, index: {unique: true} t.integer :tier, default: 0 t.integer :track, default: 0 t.integer :streak, default: 0 @@ -11,7 +11,7 @@ def change end create_table :leaderboards, id: false, primary_key: :leaderboard_id do |t| - t.string :leaderboard_id, null: false, index: { unique: true } + t.string :leaderboard_id, null: false, index: {unique: true} t.string :name, null: false t.timestamps end diff --git a/apps/govquests-api/rails_app/db/queue_schema.rb b/apps/govquests-api/rails_app/db/queue_schema.rb index 85194b6..4b2cfdb 100644 --- a/apps/govquests-api/rails_app/db/queue_schema.rb +++ b/apps/govquests-api/rails_app/db/queue_schema.rb @@ -6,24 +6,24 @@ t.string "concurrency_key", null: false t.datetime "expires_at", null: false t.datetime "created_at", null: false - t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" - t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" - t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release" + t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true end create_table "solid_queue_claimed_executions", force: :cascade do |t| t.bigint "job_id", null: false t.bigint "process_id" t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true - t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" end create_table "solid_queue_failed_executions", force: :cascade do |t| t.bigint "job_id", null: false t.text "error" t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true end create_table "solid_queue_jobs", force: :cascade do |t| @@ -37,17 +37,17 @@ t.string "concurrency_key" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" - t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" - t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" - t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" - t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering" + t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting" end create_table "solid_queue_pauses", force: :cascade do |t| t.string "queue_name", null: false t.datetime "created_at", null: false - t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true end create_table "solid_queue_processes", force: :cascade do |t| @@ -59,9 +59,9 @@ t.text "metadata" t.datetime "created_at", null: false t.string "name", null: false - t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" - t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true - t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" end create_table "solid_queue_ready_executions", force: :cascade do |t| @@ -69,9 +69,9 @@ t.string "queue_name", null: false t.integer "priority", default: 0, null: false t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true - t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" - t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index ["priority", "job_id"], name: "index_solid_queue_poll_all" + t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue" end create_table "solid_queue_recurring_executions", force: :cascade do |t| @@ -79,8 +79,8 @@ t.string "task_key", null: false t.datetime "run_at", null: false t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true - t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true end create_table "solid_queue_recurring_tasks", force: :cascade do |t| @@ -95,8 +95,8 @@ t.text "description" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true - t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" + t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static" end create_table "solid_queue_scheduled_executions", force: :cascade do |t| @@ -105,8 +105,8 @@ t.integer "priority", default: 0, null: false t.datetime "scheduled_at", null: false t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true - t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all" end create_table "solid_queue_semaphores", force: :cascade do |t| @@ -115,9 +115,9 @@ t.datetime "expires_at", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" - t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" - t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true end add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade diff --git a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb index 883ce35..d3b5ef6 100644 --- a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_created_test.rb @@ -7,7 +7,7 @@ def setup @action_id = SecureRandom.uuid @content = "Complete survey" @action_type = "ReadDocument" - @completion_criteria = { document_url: "https://example.com/survey" }.transform_keys(&:to_s) + @completion_criteria = {document_url: "https://example.com/survey"}.transform_keys(&:to_s) end test "creates a new action when handling ActionCreated event" do diff --git a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb index c9a5906..525d2d9 100644 --- a/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/action_tracking/on_action_executed_test.rb @@ -7,7 +7,7 @@ def setup @action_id = SecureRandom.uuid @user_id = SecureRandom.uuid @timestamp = Time.current - @completion_data = { "result" => "success" } + @completion_data = {"result" => "success"} end test "creates a new action log when handling ActionExecuted event" do diff --git a/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb b/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb index acc243e..da6e287 100644 --- a/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/authentication/on_user_registered_test.rb @@ -36,7 +36,7 @@ def setup user_id: @user_id, email: "old@example.com", user_type: "delegate", - wallets: [ { wallet_address: "0x0987654321fedcba", chain_id: 2 } ] + wallets: [{wallet_address: "0x0987654321fedcba", chain_id: 2}] ) existing_user.save! @@ -64,7 +64,7 @@ def setup user_id: @user_id, email: @email, user_type: @user_type, - wallets: [ { wallet_address: @wallet_address, chain_id: @chain_id } ] + wallets: [{wallet_address: @wallet_address, chain_id: @chain_id}] ) event = UserRegistered.new(data: { diff --git a/apps/govquests-api/rails_app/test/read_models/gamification/on_badge_earned_test.rb b/apps/govquests-api/rails_app/test/read_models/gamification/on_badge_earned_test.rb index f6ae09c..60830ed 100644 --- a/apps/govquests-api/rails_app/test/read_models/gamification/on_badge_earned_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/gamification/on_badge_earned_test.rb @@ -28,7 +28,7 @@ def setup test "does not duplicate badges in game profile" do profile = GameProfileReadModel.find_by(profile_id: @profile_id) - profile.update(badges: [ @badge ]) + profile.update(badges: [@badge]) event = BadgeEarned.new(data: { profile_id: @profile_id, diff --git a/apps/govquests-api/rails_app/test/read_models/gamification/on_tier_achieved_test.rb b/apps/govquests-api/rails_app/test/read_models/gamification/on_tier_achieved_test.rb index 22e0e02..51f06ee 100644 --- a/apps/govquests-api/rails_app/test/read_models/gamification/on_tier_achieved_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/gamification/on_tier_achieved_test.rb @@ -9,7 +9,7 @@ def setup end test "updates tier when handling TierAchieved event" do - event = Gamification::TierAchieved.new(data: { profile_id: @profile_id, tier: "2" }) + event = Gamification::TierAchieved.new(data: {profile_id: @profile_id, tier: "2"}) @handler.call(event) @game_profile.reload @@ -18,7 +18,7 @@ def setup test "handles non-existent game profile gracefully" do non_existent_profile_id = SecureRandom.uuid - event = Gamification::TierAchieved.new(data: { profile_id: non_existent_profile_id, tier: "2" }) + event = Gamification::TierAchieved.new(data: {profile_id: non_existent_profile_id, tier: "2"}) @handler.call(event) assert_nil GameProfileReadModel.find_by(profile_id: non_existent_profile_id) diff --git a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb index dd02b5c..d1242fd 100644 --- a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb @@ -10,7 +10,7 @@ def setup @intro = "Learn about governance basics" @quest_type = "Onboarding" @audience = "AllUsers" - @reward = { type: "Points", amount: 50 }.transform_keys(&:to_s) + @reward = {type: "Points", amount: 50}.transform_keys(&:to_s) end test "creates a new quest when handling QuestCreated event" do diff --git a/apps/govquests-api/rails_app/test/test_helper.rb b/apps/govquests-api/rails_app/test/test_helper.rb index 01efd64..a2c2d3d 100644 --- a/apps/govquests-api/rails_app/test/test_helper.rb +++ b/apps/govquests-api/rails_app/test/test_helper.rb @@ -58,26 +58,26 @@ def before_teardown def register_customer(name) customer_id = SecureRandom.uuid - post "/customers", params: { customer_id: customer_id, name: name } + post "/customers", params: {customer_id: customer_id, name: name} customer_id end def register_product(name, price, vat_rate_code) product_id = SecureRandom.uuid - post "/products", params: { product_id: product_id, name: name, price: price, vat_rate_code: vat_rate_code } + post "/products", params: {product_id: product_id, name: name, price: price, vat_rate_code: vat_rate_code} product_id end def register_coupon(name, code, discount) - post "/coupons", params: { coupon_id: SecureRandom.uuid, name: name, code: code, discount: discount } + post "/coupons", params: {coupon_id: SecureRandom.uuid, name: name, code: code, discount: discount} end def add_available_vat_rate(rate, code = rate.to_s) - post "/available_vat_rates", params: { code: code, rate: rate } + post "/available_vat_rates", params: {code: code, rate: rate} end def supply_product(product_id, quantity) - post "/products/#{product_id}/supplies", params: { quantity: quantity } + post "/products/#{product_id}/supplies", params: {quantity: quantity} end def update_price(product_id, new_price) @@ -90,7 +90,7 @@ def update_price(product_id, new_price) end def login(client_id) - post "/login", params: { client_id: client_id } + post "/login", params: {client_id: client_id} follow_redirect! end From 4fdbe55a2703d2b815a0c49cc3067c24dc618bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Fri, 4 Oct 2024 10:25:13 -0300 Subject: [PATCH 14/17] chore: connect quest details page with backend --- .../app/controllers/quests_controller.rb | 22 +++++++++++++++++-- .../questing/read_model_configuration.rb | 2 +- .../src/app/(components)/QuestDetails.tsx | 12 +++------- apps/govquests-frontend/src/types/quest.ts | 4 ++-- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb index a9fd733..d634f4b 100644 --- a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb +++ b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb @@ -10,7 +10,16 @@ def index quest_type: quest.quest_type, audience: quest.audience, status: quest.status, - rewards: quest.rewards + rewards: [quest.rewards], + actions: quest.quest_actions.map do |quest_action| + action = quest_action.action + + { + id: action.action_id, + content: action.content, + action_type: action.action_type + } + end } end @@ -29,7 +38,16 @@ def show quest_type: quest.quest_type, audience: quest.audience, status: quest.status, - rewards: quest.rewards + rewards: [quest.rewards], + actions: quest.quest_actions.map do |quest_action| + action = quest_action.action + + { + id: action.action_id, + content: action.content, + action_type: action.action_type + } + end } else render json: {error: "Quest not found"}, status: :not_found diff --git a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb index 41d5055..8b47e36 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb @@ -2,7 +2,7 @@ module Questing class QuestReadModel < ApplicationRecord self.table_name = "quests" - has_many :quest_actions, class_name: "Questing::QuestActionReadModel", foreign_key: "quest_id" + has_many :quest_actions, class_name: "Questing::QuestActionReadModel", foreign_key: "quest_id", primary_key: "quest_id" def title display_data["title"] diff --git a/apps/govquests-frontend/src/app/(components)/QuestDetails.tsx b/apps/govquests-frontend/src/app/(components)/QuestDetails.tsx index 96dcd7f..1ca4d2d 100644 --- a/apps/govquests-frontend/src/app/(components)/QuestDetails.tsx +++ b/apps/govquests-frontend/src/app/(components)/QuestDetails.tsx @@ -1,16 +1,10 @@ -import type { Step } from "@/types/quest"; +import type { Quest } from "@/types/quest"; import { ExternalLink } from "lucide-react"; import Link from "next/link"; import type React from "react"; -interface Quest { - title: string; - intro: string; - steps: Step[]; -} - interface QuestDetailsProps { - quest: Quest; + quest: Pick; } const QuestDetails: React.FC = ({ quest }) => { @@ -37,7 +31,7 @@ const QuestDetails: React.FC = ({ quest }) => {

Steps to earn

- {quest.steps.map((step) => ( + {quest.actions.map((step) => (
Date: Fri, 4 Oct 2024 11:01:32 -0300 Subject: [PATCH 15/17] chore: support multiple rewards in Quest model --- .../govquests/questing/lib/questing/commands.rb | 2 +- .../govquests-api/govquests/questing/lib/questing/events.rb | 2 +- .../govquests/questing/lib/questing/on_quest_commands.rb | 2 +- apps/govquests-api/govquests/questing/lib/questing/quest.rb | 6 +++--- apps/govquests-api/govquests/questing/test/quest_test.rb | 2 +- .../rails_app/app/controllers/quests_controller.rb | 4 ++-- .../rails_app/app/read_models/questing/on_quest_created.rb | 2 +- apps/govquests-api/rails_app/db/seeds.rb | 6 +++--- .../questing/on_quest_associated_with_action_test.rb | 4 ++-- .../test/read_models/questing/on_quest_created_test.rb | 6 +++--- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands.rb b/apps/govquests-api/govquests/questing/lib/questing/commands.rb index a7b2c70..530315b 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/commands.rb @@ -5,7 +5,7 @@ class CreateQuest < Infra::Command attribute :intro, Infra::Types::String attribute :quest_type, Infra::Types::String attribute :audience, Infra::Types::String - attribute :reward, Infra::Types::Hash + attribute :rewards, Infra::Types::Array alias_method :aggregate_id, :quest_id end diff --git a/apps/govquests-api/govquests/questing/lib/questing/events.rb b/apps/govquests-api/govquests/questing/lib/questing/events.rb index de31226..65388c5 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/events.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/events.rb @@ -5,7 +5,7 @@ class QuestCreated < Infra::Event attribute :intro, Infra::Types::String attribute :quest_type, Infra::Types::String attribute :audience, Infra::Types::String - attribute :reward, Infra::Types::Hash + attribute :rewards, Infra::Types::Array end class ActionAssociatedWithQuest < Infra::Event diff --git a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb index fc27a55..f37eef1 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb @@ -8,7 +8,7 @@ def call(command) @repository.with_aggregate(Quest, command.aggregate_id) do |quest| case command when CreateQuest - quest.create(command.title, command.intro, command.quest_type, command.audience, command.reward) + quest.create(command.title, command.intro, command.quest_type, command.audience, command.rewards) when AssociateActionWithQuest quest.associate_action(command.action_id, command.position) end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest.rb b/apps/govquests-api/govquests/questing/lib/questing/quest.rb index e74468b..47bb32c 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/quest.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/quest.rb @@ -7,14 +7,14 @@ def initialize(id) @actions = [] end - def create(title, intro, quest_type, audience, reward) + def create(title, intro, quest_type, audience, rewards) apply QuestCreated.new(data: { quest_id: @id, title: title, intro: intro, quest_type: quest_type, audience: audience, - reward: reward + rewards: rewards }) end @@ -31,7 +31,7 @@ def associate_action(action_id, position) @intro = event.data[:intro] @quest_type = event.data[:quest_type] @audience = event.data[:audience] - @reward = event.data[:reward] + @rewards = event.data[:rewards] end on ActionAssociatedWithQuest do |event| diff --git a/apps/govquests-api/govquests/questing/test/quest_test.rb b/apps/govquests-api/govquests/questing/test/quest_test.rb index 098a81e..e778edb 100644 --- a/apps/govquests-api/govquests/questing/test/quest_test.rb +++ b/apps/govquests-api/govquests/questing/test/quest_test.rb @@ -29,7 +29,7 @@ def test_create_a_new_quest assert_equal intro, event.data[:intro] assert_equal quest_type, event.data[:quest_type] assert_equal audience, event.data[:audience] - assert_equal reward, event.data[:reward] + assert_equal reward, event.data[:rewards] end def test_associate_an_action_with_a_quest diff --git a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb index d634f4b..5868527 100644 --- a/apps/govquests-api/rails_app/app/controllers/quests_controller.rb +++ b/apps/govquests-api/rails_app/app/controllers/quests_controller.rb @@ -10,7 +10,7 @@ def index quest_type: quest.quest_type, audience: quest.audience, status: quest.status, - rewards: [quest.rewards], + rewards: quest.rewards, actions: quest.quest_actions.map do |quest_action| action = quest_action.action @@ -38,7 +38,7 @@ def show quest_type: quest.quest_type, audience: quest.audience, status: quest.status, - rewards: [quest.rewards], + rewards: quest.rewards, actions: quest.quest_actions.map do |quest_action| action = quest_action.action diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb index 0105050..6d49d60 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb @@ -5,7 +5,7 @@ def call(event) quest_id: event.data[:quest_id], quest_type: event.data[:quest_type], audience: event.data[:audience], - rewards: event.data[:reward], + rewards: event.data[:rewards], status: "created", display_data: { title: event.data[:title], diff --git a/apps/govquests-api/rails_app/db/seeds.rb b/apps/govquests-api/rails_app/db/seeds.rb index 4142ccd..24aa93a 100644 --- a/apps/govquests-api/rails_app/db/seeds.rb +++ b/apps/govquests-api/rails_app/db/seeds.rb @@ -60,7 +60,7 @@ def create_action(action_data) intro: "Understand the Optimism Values and your role in governance.", quest_type: "Onboarding", audience: "AllUsers", - reward: {type: "Points", amount: 50}, + rewards: [{type: "Points", amount: 50}], actions: [ "Read the Code of Conduct", "Watch the Optimistic Vision video", @@ -73,7 +73,7 @@ def create_action(action_data) intro: "Dive deeper into governance processes and decision-making.", quest_type: "Governance", audience: "Delegates", - reward: {type: "Points", amount: 100}, + rewards: [{type: "Points", amount: 100}], actions: [ "Participate in a Mock Proposal Vote", "Analyze Governance Analytics Dashboard", @@ -91,7 +91,7 @@ def create_quest(quest_data) intro: quest_data[:intro], quest_type: quest_data[:quest_type], audience: quest_data[:audience], - reward: quest_data[:reward] + rewards: quest_data[:rewards] ) Rails.configuration.command_bus.call(command) quest_id diff --git a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_associated_with_action_test.rb b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_associated_with_action_test.rb index cf63f42..96c2c59 100644 --- a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_associated_with_action_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_associated_with_action_test.rb @@ -8,8 +8,8 @@ def setup @action_id = SecureRandom.uuid @position = 1 - QuestReadModel.create!(quest_id: @quest_id, title: "Test Quest", intro: "Test Intro", quest_type: "Test", audience: "All", status: "created") - ActionTracking::ActionReadModel.create!(action_id: @action_id, content: "Test Action", action_type: "Test", completion_criteria: {}) + QuestReadModel.create!(quest_id: @quest_id, display_data: {title: "Test Quest", intro: "Test Intro"}, quest_type: "Test", audience: "All", status: "created") + ActionTracking::ActionReadModel.create!(action_id: @action_id, display_data: {content: "Test Action"}, action_type: "Test", completion_criteria: {}) end test "associates an action with a quest when handling ActionAssociatedWithQuest event" do diff --git a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb index d1242fd..93f1227 100644 --- a/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb +++ b/apps/govquests-api/rails_app/test/read_models/questing/on_quest_created_test.rb @@ -10,7 +10,7 @@ def setup @intro = "Learn about governance basics" @quest_type = "Onboarding" @audience = "AllUsers" - @reward = {type: "Points", amount: 50}.transform_keys(&:to_s) + @rewards = [{type: "Points", amount: 50}.transform_keys(&:to_s)] end test "creates a new quest when handling QuestCreated event" do @@ -20,7 +20,7 @@ def setup intro: @intro, quest_type: @quest_type, audience: @audience, - reward: @reward + rewards: @rewards }) assert_difference -> { QuestReadModel.count }, 1 do @@ -32,7 +32,7 @@ def setup assert_equal @intro, quest.intro assert_equal @quest_type, quest.quest_type assert_equal @audience, quest.audience - assert_equal @reward, quest.rewards + assert_equal @rewards, quest.rewards assert_equal "created", quest.status end end From 5a15a6bb744a82ffe5f83e05f92722f1e41d5920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Fri, 4 Oct 2024 13:12:04 -0300 Subject: [PATCH 16/17] feat: adapt display_data to allow any parameter --- apps/govquests-api/Makefile | 2 +- .../govquests/action_tracking/.mutant.yml | 4 +- .../govquests/action_tracking/README.md | 2 +- .../lib/action_tracking/action.rb | 11 +- .../lib/action_tracking/on_action_commands.rb | 2 + .../action_tracking/test/action_test.rb | 18 +-- .../action_tracking/test/test_helper.rb | 2 +- .../govquests/gamification/.mutant.yml | 4 +- .../govquests/gamification/README.md | 2 +- .../gamification/lib/gamification.rb | 1 + .../lib/gamification/game_profile.rb | 1 - .../lib/gamification/leaderboard.rb | 1 - .../gamification/test/leaderboard_test.rb | 30 +++++ .../gamification/test/test_helper.rb | 2 +- .../govquests/notifications/.mutant.yml | 4 +- .../govquests/notifications/README.md | 2 +- .../notifications/test/test_helper.rb | 2 +- .../questing/lib/questing/commands.rb | 3 +- .../govquests/questing/lib/questing/events.rb | 3 +- .../lib/questing/on_quest_commands.rb | 2 +- .../govquests/questing/lib/questing/quest.rb | 8 +- .../govquests/questing/test/quest_test.rb | 8 +- .../govquests/rewarding/.mutant.yml | 4 +- .../govquests/rewarding/README.md | 2 +- .../govquests/rewarding/test/reward_test.rb | 2 +- .../govquests/rewarding/test/test_helper.rb | 2 +- .../action_tracking/on_action_created.rb | 7 +- .../action_tracking/on_action_executed.rb | 22 ++-- .../read_models/questing/on_quest_created.rb | 6 +- apps/govquests-api/rails_app/db/seeds.rb | 13 ++- .../test/integration/action_flow_test.rb | 41 +++++++ .../test/integration/reward_claim_flow.rb | 47 ++++++++ .../test/integration/user_auth_flow_test.rb | 47 ++++++++ .../user_quest_reward_flow_test.rb | 108 ++++++++++++++++++ .../on_notification_scheduled_test.rb | 35 ++++++ .../rails_app/test/test_helper.rb | 10 ++ .../src/app/(components)/QuestsList.tsx | 14 +-- apps/govquests-frontend/src/types/quest.ts | 2 +- 38 files changed, 393 insertions(+), 83 deletions(-) create mode 100644 apps/govquests-api/govquests/gamification/test/leaderboard_test.rb create mode 100644 apps/govquests-api/rails_app/test/integration/action_flow_test.rb create mode 100644 apps/govquests-api/rails_app/test/integration/reward_claim_flow.rb create mode 100644 apps/govquests-api/rails_app/test/integration/user_auth_flow_test.rb create mode 100644 apps/govquests-api/rails_app/test/integration/user_quest_reward_flow_test.rb create mode 100644 apps/govquests-api/rails_app/test/read_models/notification/on_notification_scheduled_test.rb diff --git a/apps/govquests-api/Makefile b/apps/govquests-api/Makefile index 37a5bca..7be3e80 100644 --- a/apps/govquests-api/Makefile +++ b/apps/govquests-api/Makefile @@ -1,5 +1,5 @@ # Define the subdirectories where tests should be run -SUBDIRS := govquests/authentication govquests/questing infra rails_app +SUBDIRS := govquests/authentication govquests/questing govquests/gamification govquests/notifications govquests/rewarding govquests/action_tracking infra rails_app .PHONY: test $(SUBDIRS) diff --git a/apps/govquests-api/govquests/action_tracking/.mutant.yml b/apps/govquests-api/govquests/action_tracking/.mutant.yml index a4ea1c5..bd3275d 100644 --- a/apps/govquests-api/govquests/action_tracking/.mutant.yml +++ b/apps/govquests-api/govquests/action_tracking/.mutant.yml @@ -6,6 +6,6 @@ coverage_criteria: process_abort: true matcher: subjects: - - Questing* + - ActionTracking* ignore: - - Questing::Configuration#call + - ActionTracking::Configuration#call diff --git a/apps/govquests-api/govquests/action_tracking/README.md b/apps/govquests-api/govquests/action_tracking/README.md index b187021..30ac2c8 100644 --- a/apps/govquests-api/govquests/action_tracking/README.md +++ b/apps/govquests-api/govquests/action_tracking/README.md @@ -1,4 +1,4 @@ -# Questing +# ActionTracking #### Up and running diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb index 089623e..7a7c844 100644 --- a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/action.rb @@ -4,6 +4,10 @@ class Action def initialize(id) @id = id + @content = nil + @action_type = nil + @completion_criteria = {} + @completed = false end def create(content, action_type, completion_criteria) @@ -16,6 +20,8 @@ def create(content, action_type, completion_criteria) end def complete(user_id, completion_data) + raise "Action already completed" if @completed + apply ActionExecuted.new(data: { action_id: @id, user_id: user_id, @@ -23,6 +29,8 @@ def complete(user_id, completion_data) }) end + private + on ActionCreated do |event| @content = event.data[:content] @action_type = event.data[:action_type] @@ -30,7 +38,8 @@ def complete(user_id, completion_data) end on ActionExecuted do |event| - # Handle completion logic if needed + @completed = true + # Additional logic for completion can be added here end end end diff --git a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb index 98fd076..cef67c5 100644 --- a/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb +++ b/apps/govquests-api/govquests/action_tracking/lib/action_tracking/on_action_commands.rb @@ -11,6 +11,8 @@ def call(command) action.create(command.content, command.action_type, command.completion_criteria) when CompleteAction action.complete(command.user_id, command.completion_data) + else + raise "Unknown command: #{command.class}" end end end diff --git a/apps/govquests-api/govquests/action_tracking/test/action_test.rb b/apps/govquests-api/govquests/action_tracking/test/action_test.rb index b374ca6..4a02573 100644 --- a/apps/govquests-api/govquests/action_tracking/test/action_test.rb +++ b/apps/govquests-api/govquests/action_tracking/test/action_test.rb @@ -12,9 +12,10 @@ def setup def test_create_a_new_action content = "Complete survey" - channel = "Email" + action_type = "ReadDocument" + completion_criteria = {document_url: "https://example.com/survey"} - @action.create(content, channel) + @action.create(content, action_type, completion_criteria) events = @action.unpublished_events.to_a assert_equal 1, events.size @@ -22,15 +23,16 @@ def test_create_a_new_action assert_instance_of ActionCreated, event assert_equal @action_id, event.data[:action_id] assert_equal content, event.data[:content] - assert_equal channel, event.data[:channel] + assert_equal action_type, event.data[:action_type] + assert_equal completion_criteria, event.data[:completion_criteria] end - def test_execute_an_action - @action.create("Complete survey", "High", "Email") + def test_complete_an_action + @action.create("Complete survey", "ReadDocument", {document_url: "https://example.com/survey"}) user_id = SecureRandom.uuid - timestamp = Time.now + completion_data = {result: "success"} - @action.execute(user_id, timestamp) + @action.complete(user_id, completion_data) events = @action.unpublished_events.to_a assert_equal 2, events.size @@ -38,7 +40,7 @@ def test_execute_an_action assert_instance_of ActionExecuted, event assert_equal @action_id, event.data[:action_id] assert_equal user_id, event.data[:user_id] - assert_equal timestamp, event.data[:timestamp] + assert_equal completion_data, event.data[:completion_data] end end end diff --git a/apps/govquests-api/govquests/action_tracking/test/test_helper.rb b/apps/govquests-api/govquests/action_tracking/test/test_helper.rb index a8fadc8..824fffe 100644 --- a/apps/govquests-api/govquests/action_tracking/test/test_helper.rb +++ b/apps/govquests-api/govquests/action_tracking/test/test_helper.rb @@ -3,7 +3,7 @@ require_relative "../lib/action_tracking" -module Questing +module ActionTracking class Test < Infra::InMemoryTest def before_setup super diff --git a/apps/govquests-api/govquests/gamification/.mutant.yml b/apps/govquests-api/govquests/gamification/.mutant.yml index a4ea1c5..1e01ad8 100644 --- a/apps/govquests-api/govquests/gamification/.mutant.yml +++ b/apps/govquests-api/govquests/gamification/.mutant.yml @@ -6,6 +6,6 @@ coverage_criteria: process_abort: true matcher: subjects: - - Questing* + - Gamification* ignore: - - Questing::Configuration#call + - Gamification::Configuration#call diff --git a/apps/govquests-api/govquests/gamification/README.md b/apps/govquests-api/govquests/gamification/README.md index b187021..17b7b91 100644 --- a/apps/govquests-api/govquests/gamification/README.md +++ b/apps/govquests-api/govquests/gamification/README.md @@ -1,4 +1,4 @@ -# Questing +# Gamification #### Up and running diff --git a/apps/govquests-api/govquests/gamification/lib/gamification.rb b/apps/govquests-api/govquests/gamification/lib/gamification.rb index bc7a3cd..428eb13 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification.rb @@ -3,6 +3,7 @@ require_relative "gamification/events" require_relative "gamification/on_game_profile_commands" require_relative "gamification/game_profile" +require_relative "gamification/leaderboard" module Gamification class Configuration diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/game_profile.rb b/apps/govquests-api/govquests/gamification/lib/gamification/game_profile.rb index 0ed2df3..86e2b42 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification/game_profile.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification/game_profile.rb @@ -1,4 +1,3 @@ -# govquests/gamification/lib/gamification/game_profile.rb module Gamification class GameProfile include AggregateRoot diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/leaderboard.rb b/apps/govquests-api/govquests/gamification/lib/gamification/leaderboard.rb index fbe8776..c72637c 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification/leaderboard.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification/leaderboard.rb @@ -1,4 +1,3 @@ -# govquests/gamification/lib/gamification/leaderboard.rb module Gamification class Leaderboard include AggregateRoot diff --git a/apps/govquests-api/govquests/gamification/test/leaderboard_test.rb b/apps/govquests-api/govquests/gamification/test/leaderboard_test.rb new file mode 100644 index 0000000..d99981a --- /dev/null +++ b/apps/govquests-api/govquests/gamification/test/leaderboard_test.rb @@ -0,0 +1,30 @@ +require_relative "test_helper" + +module Gamification + class LeaderboardTest < Test + cover "Gamification::Leaderboard" + + def setup + super + @leaderboard_id = SecureRandom.uuid + @leaderboard = Leaderboard.new(@leaderboard_id) + end + + def test_update_score_and_rank + profile_id1 = SecureRandom.uuid + profile_id2 = SecureRandom.uuid + profile_id3 = SecureRandom.uuid + + # Update scores + @leaderboard.update_score(profile_id1, 300) + @leaderboard.update_score(profile_id2, 500) + @leaderboard.update_score(profile_id3, 300) + + events = @leaderboard.unpublished_events.to_a + assert_equal 3, events.size + assert_instance_of LeaderboardUpdated, events[0] + assert_instance_of LeaderboardUpdated, events[1] + assert_instance_of LeaderboardUpdated, events[2] + end + end +end diff --git a/apps/govquests-api/govquests/gamification/test/test_helper.rb b/apps/govquests-api/govquests/gamification/test/test_helper.rb index d4b14e0..4d81186 100644 --- a/apps/govquests-api/govquests/gamification/test/test_helper.rb +++ b/apps/govquests-api/govquests/gamification/test/test_helper.rb @@ -3,7 +3,7 @@ require_relative "../lib/gamification" -module Questing +module Gamification class Test < Infra::InMemoryTest def before_setup super diff --git a/apps/govquests-api/govquests/notifications/.mutant.yml b/apps/govquests-api/govquests/notifications/.mutant.yml index a4ea1c5..16fc1cf 100644 --- a/apps/govquests-api/govquests/notifications/.mutant.yml +++ b/apps/govquests-api/govquests/notifications/.mutant.yml @@ -6,6 +6,6 @@ coverage_criteria: process_abort: true matcher: subjects: - - Questing* + - Notifications* ignore: - - Questing::Configuration#call + - Notifications::Configuration#call diff --git a/apps/govquests-api/govquests/notifications/README.md b/apps/govquests-api/govquests/notifications/README.md index b187021..d45bc12 100644 --- a/apps/govquests-api/govquests/notifications/README.md +++ b/apps/govquests-api/govquests/notifications/README.md @@ -1,4 +1,4 @@ -# Questing +# Notifications #### Up and running diff --git a/apps/govquests-api/govquests/notifications/test/test_helper.rb b/apps/govquests-api/govquests/notifications/test/test_helper.rb index f824b17..001a0e3 100644 --- a/apps/govquests-api/govquests/notifications/test/test_helper.rb +++ b/apps/govquests-api/govquests/notifications/test/test_helper.rb @@ -3,7 +3,7 @@ require_relative "../lib/notifications" -module Questing +module Notifications class Test < Infra::InMemoryTest def before_setup super diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands.rb b/apps/govquests-api/govquests/questing/lib/questing/commands.rb index 530315b..b3d02b6 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/commands.rb @@ -1,8 +1,7 @@ module Questing class CreateQuest < Infra::Command attribute :quest_id, Infra::Types::UUID - attribute :title, Infra::Types::String - attribute :intro, Infra::Types::String + attribute :display_data, Infra::Types::Hash attribute :quest_type, Infra::Types::String attribute :audience, Infra::Types::String attribute :rewards, Infra::Types::Array diff --git a/apps/govquests-api/govquests/questing/lib/questing/events.rb b/apps/govquests-api/govquests/questing/lib/questing/events.rb index 65388c5..f22c98d 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/events.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/events.rb @@ -1,8 +1,7 @@ module Questing class QuestCreated < Infra::Event attribute :quest_id, Infra::Types::UUID - attribute :title, Infra::Types::String - attribute :intro, Infra::Types::String + attribute :display_data, Infra::Types::Hash attribute :quest_type, Infra::Types::String attribute :audience, Infra::Types::String attribute :rewards, Infra::Types::Array diff --git a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb index f37eef1..6b0846a 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/on_quest_commands.rb @@ -8,7 +8,7 @@ def call(command) @repository.with_aggregate(Quest, command.aggregate_id) do |quest| case command when CreateQuest - quest.create(command.title, command.intro, command.quest_type, command.audience, command.rewards) + quest.create(command.display_data, command.quest_type, command.audience, command.rewards) when AssociateActionWithQuest quest.associate_action(command.action_id, command.position) end diff --git a/apps/govquests-api/govquests/questing/lib/questing/quest.rb b/apps/govquests-api/govquests/questing/lib/questing/quest.rb index 47bb32c..47826ba 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/quest.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/quest.rb @@ -7,11 +7,10 @@ def initialize(id) @actions = [] end - def create(title, intro, quest_type, audience, rewards) + def create(display_data, quest_type, audience, rewards) apply QuestCreated.new(data: { quest_id: @id, - title: title, - intro: intro, + display_data: display_data, quest_type: quest_type, audience: audience, rewards: rewards @@ -27,8 +26,7 @@ def associate_action(action_id, position) end on QuestCreated do |event| - @title = event.data[:title] - @intro = event.data[:intro] + @display_data = event.data[:display_data] @quest_type = event.data[:quest_type] @audience = event.data[:audience] @rewards = event.data[:rewards] diff --git a/apps/govquests-api/govquests/questing/test/quest_test.rb b/apps/govquests-api/govquests/questing/test/quest_test.rb index e778edb..8ef9c1e 100644 --- a/apps/govquests-api/govquests/questing/test/quest_test.rb +++ b/apps/govquests-api/govquests/questing/test/quest_test.rb @@ -16,9 +16,9 @@ def test_create_a_new_quest intro = "Learn about governance basics" quest_type = "Standard" audience = "AllUsers" - reward = {type: "points", value: 100} + rewards = [{type: "points", value: 100}] - @quest.create(title, intro, quest_type, audience, reward) + @quest.create(title, intro, quest_type, audience, rewards) events = @quest.unpublished_events.to_a assert_equal 1, events.size @@ -29,11 +29,11 @@ def test_create_a_new_quest assert_equal intro, event.data[:intro] assert_equal quest_type, event.data[:quest_type] assert_equal audience, event.data[:audience] - assert_equal reward, event.data[:rewards] + assert_equal rewards, event.data[:rewards] end def test_associate_an_action_with_a_quest - @quest.create("Test Quest", "Test Intro", "Standard", "AllUsers", {type: "points", value: 10}) + @quest.create("Test Quest", "Test Intro", "Standard", "AllUsers", [{type: "points", value: 10}]) action_id = SecureRandom.uuid position = 1 diff --git a/apps/govquests-api/govquests/rewarding/.mutant.yml b/apps/govquests-api/govquests/rewarding/.mutant.yml index a4ea1c5..9db3b5c 100644 --- a/apps/govquests-api/govquests/rewarding/.mutant.yml +++ b/apps/govquests-api/govquests/rewarding/.mutant.yml @@ -6,6 +6,6 @@ coverage_criteria: process_abort: true matcher: subjects: - - Questing* + - Rewarding* ignore: - - Questing::Configuration#call + - Rewarding::Configuration#call diff --git a/apps/govquests-api/govquests/rewarding/README.md b/apps/govquests-api/govquests/rewarding/README.md index b187021..3445253 100644 --- a/apps/govquests-api/govquests/rewarding/README.md +++ b/apps/govquests-api/govquests/rewarding/README.md @@ -1,4 +1,4 @@ -# Questing +# Rewarding #### Up and running diff --git a/apps/govquests-api/govquests/rewarding/test/reward_test.rb b/apps/govquests-api/govquests/rewarding/test/reward_test.rb index 9ec0c3f..77e7494 100644 --- a/apps/govquests-api/govquests/rewarding/test/reward_test.rb +++ b/apps/govquests-api/govquests/rewarding/test/reward_test.rb @@ -13,7 +13,7 @@ def setup def test_create_a_new_reward reward_type = "points" value = 100 - expiry_date = Time.now + 30.days + expiry_date = Time.now + 30 * 24 * 60 * 60 @reward.create(reward_type, value, expiry_date) diff --git a/apps/govquests-api/govquests/rewarding/test/test_helper.rb b/apps/govquests-api/govquests/rewarding/test/test_helper.rb index ea90990..d062fe1 100644 --- a/apps/govquests-api/govquests/rewarding/test/test_helper.rb +++ b/apps/govquests-api/govquests/rewarding/test/test_helper.rb @@ -3,7 +3,7 @@ require_relative "../lib/rewarding" -module Questing +module Rewarding class Test < Infra::InMemoryTest def before_setup super diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb index f9cc1da..2b3f465 100644 --- a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_created.rb @@ -1,16 +1,15 @@ module ActionTracking class OnActionCreated def call(event) - action = ActionReadModel.create!( + ActionReadModel.create!( action_id: event.data[:action_id], action_type: event.data[:action_type], completion_criteria: event.data[:completion_criteria], display_data: { - content: event.data[:content], - duration: event.data[:duration] + content: event.data[:content] } ) - Rails.logger.info "Action created in read model: #{action.action_id}" + Rails.logger.info "Action created in read model: #{event.data[:action_id]}" rescue ActiveRecord::RecordInvalid => e Rails.logger.error "Failed to create action in read model: #{e.message}" end diff --git a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb index 55095fc..b099d30 100644 --- a/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb +++ b/apps/govquests-api/rails_app/app/read_models/action_tracking/on_action_executed.rb @@ -2,21 +2,17 @@ module ActionTracking class OnActionExecuted def call(event) - action_log_id = SecureRandom.uuid - action_id = event.data[:action_id] - user_id = event.data[:user_id] - timestamp = event.data[:timestamp] - completion_data = event.data[:completion_data] - status = "Executed" - ActionLogReadModel.create!( - action_log_id: action_log_id, - action_id: action_id, - user_id: user_id, - executed_at: timestamp, - completion_data: completion_data, - status: status + action_log_id: SecureRandom.uuid, + action_id: event.data[:action_id], + user_id: event.data[:user_id], + executed_at: event.data[:completion_data][:timestamp] || Time.current, + completion_data: event.data[:completion_data], + status: "Executed" ) + Rails.logger.info "Action executed and logged: #{event.data[:action_id]} by User #{event.data[:user_id]}" + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error "Failed to create action log: #{e.message}" end end end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb index 6d49d60..c3eb066 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_quest_created.rb @@ -7,11 +7,7 @@ def call(event) audience: event.data[:audience], rewards: event.data[:rewards], status: "created", - display_data: { - title: event.data[:title], - intro: event.data[:intro], - image_url: event.data[:image_url] - } + display_data: event.data[:display_data] ) Rails.logger.info "Quest created in read model: #{quest.quest_id}" rescue ActiveRecord::RecordInvalid => e diff --git a/apps/govquests-api/rails_app/db/seeds.rb b/apps/govquests-api/rails_app/db/seeds.rb index 24aa93a..d29ce00 100644 --- a/apps/govquests-api/rails_app/db/seeds.rb +++ b/apps/govquests-api/rails_app/db/seeds.rb @@ -56,8 +56,9 @@ def create_action(action_data) quests_data = [ { - title: "Governance 101", - intro: "Understand the Optimism Values and your role in governance.", + display_data: {title: "Governance 101", + intro: "Understand the Optimism Values and your role in governance.", + image_url: "https://miro.medium.com/v2/resize:fit:810/1*jodPL02xnDRKg_7rV93QSA.jpeg"}, quest_type: "Onboarding", audience: "AllUsers", rewards: [{type: "Points", amount: 50}], @@ -69,8 +70,9 @@ def create_action(action_data) ] }, { - title: "Advanced Governance Practices", - intro: "Dive deeper into governance processes and decision-making.", + display_data: {title: "Advanced Governance Practices", + intro: "Dive deeper into governance processes and decision-making.", + image_url: "https://miro.medium.com/v2/resize:fit:810/1*jodPL02xnDRKg_7rV93QSA.jpeg"}, quest_type: "Governance", audience: "Delegates", rewards: [{type: "Points", amount: 100}], @@ -87,8 +89,7 @@ def create_quest(quest_data) quest_id = SecureRandom.uuid command = Questing::CreateQuest.new( quest_id: quest_id, - title: quest_data[:title], - intro: quest_data[:intro], + display_data: quest_data[:display_data], quest_type: quest_data[:quest_type], audience: quest_data[:audience], rewards: quest_data[:rewards] diff --git a/apps/govquests-api/rails_app/test/integration/action_flow_test.rb b/apps/govquests-api/rails_app/test/integration/action_flow_test.rb new file mode 100644 index 0000000..dab0464 --- /dev/null +++ b/apps/govquests-api/rails_app/test/integration/action_flow_test.rb @@ -0,0 +1,41 @@ +# rails_app/test/integration/action_flow_test.rb +require "test_helper" + +class ActionFlowTest < IntegrationTest + test "create and execute an action" do + # Create a new action + action_id = SecureRandom.uuid + post "/actions", params: { + action_id: action_id, + content: "Complete Governance Survey", + action_type: "ReadDocument", + completion_criteria: {document_url: "https://example.com/survey"} + } + assert_response :success + assert_equal 1, ActionReadModel.count + + action = ActionReadModel.find_by(action_id: action_id) + assert_not_nil action + assert_equal "Complete Governance Survey", action.content + assert_equal "ReadDocument", action.action_type + assert_equal({"document_url" => "https://example.com/survey"}, action.completion_criteria) + + # Execute the action + user = UserReadModel.first + SecureRandom.uuid + post "/actions/execute", params: { + action_id: action_id, + user_id: user.user_id, + timestamp: Time.current, + completion_data: {result: "success"} + } + assert_response :success + assert_equal 1, ActionLogReadModel.count + + action_log = ActionLogReadModel.last + assert_equal action_id, action_log.action_id + assert_equal user.user_id, action_log.user_id + assert_equal "Executed", action_log.status + assert_equal "success", action_log.completion_data["result"] + end +end diff --git a/apps/govquests-api/rails_app/test/integration/reward_claim_flow.rb b/apps/govquests-api/rails_app/test/integration/reward_claim_flow.rb new file mode 100644 index 0000000..2624814 --- /dev/null +++ b/apps/govquests-api/rails_app/test/integration/reward_claim_flow.rb @@ -0,0 +1,47 @@ +# rails_app/test/integration/reward_claim_flow_test.rb +require "test_helper" + +class RewardClaimFlowTest < IntegrationTest + test "issue and claim a reward" do + # Setup: Create user and reward + user_id = SecureRandom.uuid + post "/register", params: { + user_id: user_id, + email: "user3@example.com", + user_type: "non_delegate", + wallet_address: "0xA1B2C3D4E5F6G7H8", + chain_id: 1 + } + assert_response :success + + reward_id = SecureRandom.uuid + post "/rewards", params: { + reward_id: reward_id, + reward_type: "points", + value: 100, + expiry_date: (Time.current + 30.days).iso8601 + } + assert_response :success + assert_equal 1, RewardReadModel.count + + # Issue the reward to the user + post "/rewards/#{reward_id}/issue", params: {user_id: user_id} + assert_response :success + reward = RewardReadModel.find_by(reward_id: reward_id) + assert_not_nil reward + assert_equal "Issued", reward.delivery_status + assert_equal user_id, reward.issued_to + + # User claims the reward + post "/rewards/#{reward_id}/claim", params: {user_id: user_id} + assert_response :success + reward.reload + assert_equal true, reward.claimed + assert_equal "Claimed", reward.delivery_status + + # Attempt to claim again + post "/rewards/#{reward_id}/claim", params: {user_id: user_id} + assert_response :unprocessable_entity + assert_equal "Claimed", reward.delivery_status + end +end diff --git a/apps/govquests-api/rails_app/test/integration/user_auth_flow_test.rb b/apps/govquests-api/rails_app/test/integration/user_auth_flow_test.rb new file mode 100644 index 0000000..17ca1c9 --- /dev/null +++ b/apps/govquests-api/rails_app/test/integration/user_auth_flow_test.rb @@ -0,0 +1,47 @@ +# rails_app/test/integration/user_auth_flow_test.rb +require "test_helper" + +class UserAuthFlowTest < IntegrationTest + test "user registration and login" do + # Register a new user + post "/register", params: { + user_id: SecureRandom.uuid, + email: "user1@example.com", + user_type: "non_delegate", + wallet_address: "0xABCDEF1234567890", + chain_id: 1 + } + assert_response :success + assert_equal 1, UserReadModel.count + + user = UserReadModel.find_by(email: "user1@example.com") + assert_not_nil user + assert_equal "non_delegate", user.user_type + assert_equal "0xABCDEF1234567890", user.wallets.first["wallet_address"] + + # Attempt duplicate registration + post "/register", params: { + user_id: SecureRandom.uuid, + email: "user1@example.com", + user_type: "delegate", + wallet_address: "0x1234567890ABCDEF", + chain_id: 2 + } + assert_response :unprocessable_entity + assert_equal 1, UserReadModel.count # No new user created + + # Log in the user + post "/login", params: { + user_id: user.user_id, + session_token: SecureRandom.hex(16), + timestamp: Time.current + } + assert_response :success + assert_equal 1, SessionReadModel.count + + session = SessionReadModel.last + assert_equal user.user_id, session.user_id + assert_not_nil session.session_token + assert_nil session.logged_out_at + end +end diff --git a/apps/govquests-api/rails_app/test/integration/user_quest_reward_flow_test.rb b/apps/govquests-api/rails_app/test/integration/user_quest_reward_flow_test.rb new file mode 100644 index 0000000..b916665 --- /dev/null +++ b/apps/govquests-api/rails_app/test/integration/user_quest_reward_flow_test.rb @@ -0,0 +1,108 @@ +# rails_app/test/integration/user_quest_reward_flow_test.rb +require "test_helper" + +class UserQuestRewardFlowTest < IntegrationTest + test "user completes quest and earns rewards" do + # Setup: Create user, actions, and quest + user_id = SecureRandom.uuid + post "/register", params: { + user_id: user_id, + email: "user2@example.com", + user_type: "delegate", + wallet_address: "0xFEDCBA0987654321", + chain_id: 1 + } + assert_response :success + + quest_id = SecureRandom.uuid + post "/quests", params: { + quest_id: quest_id, + title: "Governance 101", + intro: "Learn the basics of governance", + quest_type: "Onboarding", + audience: "AllUsers", + rewards: [{type: "points", amount: 50}] + } + assert_response :success + + action1_id = SecureRandom.uuid + post "/actions", params: { + action_id: action1_id, + content: "Read the Governance Policy", + action_type: "ReadDocument", + completion_criteria: {document_url: "https://example.com/governance"} + } + assert_response :success + + action2_id = SecureRandom.uuid + post "/actions", params: { + action_id: action2_id, + content: "Vote on Proposal", + action_type: "Vote", + completion_criteria: {proposal_id: "proposal_123"} + } + assert_response :success + + post "/quests/#{quest_id}/actions", params: { + action_id: action1_id, + position: 1 + } + assert_response :success + + post "/quests/#{quest_id}/actions", params: { + action_id: action2_id, + position: 2 + } + assert_response :success + + # User starts the quest + post "/quests/#{quest_id}/start", params: {user_id: user_id} + assert_response :success + assert_equal 1, UserQuest.count + + user_quest = UserQuest.find_by(quest_id: quest_id, user_id: user_id) + assert_not_nil user_quest + assert_equal "started", user_quest.status + + # User completes first action + post "/actions/execute", params: { + action_id: action1_id, + user_id: user_id, + timestamp: Time.current, + completion_data: {result: "success"} + } + assert_response :success + assert_equal 1, ActionLogReadModel.count + + # User completes second action + post "/actions/execute", params: { + action_id: action2_id, + user_id: user_id, + timestamp: Time.current, + completion_data: {result: "success"} + } + assert_response :success + assert_equal 2, ActionLogReadModel.count + + # Verify quest completion and reward issuance + # Assuming completing all actions in a quest triggers reward creation + # This might require observing emitted events or checking read models + + # Check GameProfile for earned points + game_profile = GameProfileReadModel.find_by(profile_id: user_id) + assert_not_nil game_profile + assert_equal 50, game_profile.score + + # Check Reward issuance + reward = RewardReadModel.find_by(issued_to: user_id) + assert_not_nil reward + assert_equal "points", reward.reward_type + assert_equal 50, reward.value + assert_equal "Issued", reward.delivery_status + + # Check Badge earning (if applicable) + # This depends on your gamification rules + # Example: + # assert_includes game_profile.badges, "First Quest Completed" + end +end diff --git a/apps/govquests-api/rails_app/test/read_models/notification/on_notification_scheduled_test.rb b/apps/govquests-api/rails_app/test/read_models/notification/on_notification_scheduled_test.rb new file mode 100644 index 0000000..6439ebd --- /dev/null +++ b/apps/govquests-api/rails_app/test/read_models/notification/on_notification_scheduled_test.rb @@ -0,0 +1,35 @@ +require "test_helper" + +module Notifications + class OnNotificationScheduledTest < ActiveSupport::TestCase + def setup + @handler = OnNotificationScheduled.new + @notification_id = SecureRandom.uuid + @scheduled_time = Time.current + 1.day + + NotificationReadModel.create!( + notification_id: @notification_id, + template_id: SecureRandom.uuid, + user_id: SecureRandom.uuid, + channel: "email", + priority: 2, + content: "Your weekly summary is ready.", + notification_type: "summary", + status: "created" + ) + end + + test "schedules a notification when handling NotificationScheduled event" do + event = NotificationScheduled.new(data: { + notification_id: @notification_id, + scheduled_time: @scheduled_time + }) + + @handler.call(event) + + notification = NotificationReadModel.find_by(notification_id: @notification_id) + assert_equal @scheduled_time.to_i, notification.scheduled_time.to_i + assert_equal "scheduled", notification.status + end + end +end diff --git a/apps/govquests-api/rails_app/test/test_helper.rb b/apps/govquests-api/rails_app/test/test_helper.rb index a2c2d3d..cab7e88 100644 --- a/apps/govquests-api/rails_app/test/test_helper.rb +++ b/apps/govquests-api/rails_app/test/test_helper.rb @@ -128,3 +128,13 @@ def run_command(command) Rails.configuration.command_bus.call(command) end end + +class IntegrationTest < ActionDispatch::IntegrationTest + # Setup runs before each test + def setup + super + # Reset the database or use transactional fixtures if necessary + Rails.application.load_tasks + # Rake::Task["db:reset"].invoke + end +end diff --git a/apps/govquests-frontend/src/app/(components)/QuestsList.tsx b/apps/govquests-frontend/src/app/(components)/QuestsList.tsx index c887a56..3680b8d 100644 --- a/apps/govquests-frontend/src/app/(components)/QuestsList.tsx +++ b/apps/govquests-frontend/src/app/(components)/QuestsList.tsx @@ -1,17 +1,9 @@ import Quest from "@/components/Quest"; -import type { Reward } from "@/types/quest"; +import type { Quest as IQuest, Reward } from "@/types/quest"; import type React from "react"; -interface QuestProps { - id: string; - title: string; - img_url: string; - rewards: Reward[]; - status: string; -} - interface QuestsListProps { - quests: QuestProps[]; + quests: Pick[]; } const QuestsList: React.FC = ({ quests }) => { @@ -28,7 +20,7 @@ const QuestsList: React.FC = ({ quests }) => { id={quest.id} title={quest.title} altText={quest.title} - imageSrc={quest.img_url} + imageSrc={quest.image_url} rewards={quest.rewards} status={quest.status} /> diff --git a/apps/govquests-frontend/src/types/quest.ts b/apps/govquests-frontend/src/types/quest.ts index ce22fe7..c6ff561 100644 --- a/apps/govquests-frontend/src/types/quest.ts +++ b/apps/govquests-frontend/src/types/quest.ts @@ -10,7 +10,7 @@ export interface Reward { export interface Quest { id: string; - img_url: string; + image_url: string; title: string; rewards: Reward[]; intro: string; From 23fa0c2350cdb1fa4ecbdc4f487c08c46829bc5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ribeiro?= Date: Fri, 4 Oct 2024 14:20:57 -0300 Subject: [PATCH 17/17] chore: remove integration tests for now --- .../test/integration/action_flow_test.rb | 41 ------- .../test/integration/reward_claim_flow.rb | 47 -------- .../test/integration/user_auth_flow_test.rb | 47 -------- .../user_quest_reward_flow_test.rb | 108 ------------------ 4 files changed, 243 deletions(-) delete mode 100644 apps/govquests-api/rails_app/test/integration/action_flow_test.rb delete mode 100644 apps/govquests-api/rails_app/test/integration/reward_claim_flow.rb delete mode 100644 apps/govquests-api/rails_app/test/integration/user_auth_flow_test.rb delete mode 100644 apps/govquests-api/rails_app/test/integration/user_quest_reward_flow_test.rb diff --git a/apps/govquests-api/rails_app/test/integration/action_flow_test.rb b/apps/govquests-api/rails_app/test/integration/action_flow_test.rb deleted file mode 100644 index dab0464..0000000 --- a/apps/govquests-api/rails_app/test/integration/action_flow_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -# rails_app/test/integration/action_flow_test.rb -require "test_helper" - -class ActionFlowTest < IntegrationTest - test "create and execute an action" do - # Create a new action - action_id = SecureRandom.uuid - post "/actions", params: { - action_id: action_id, - content: "Complete Governance Survey", - action_type: "ReadDocument", - completion_criteria: {document_url: "https://example.com/survey"} - } - assert_response :success - assert_equal 1, ActionReadModel.count - - action = ActionReadModel.find_by(action_id: action_id) - assert_not_nil action - assert_equal "Complete Governance Survey", action.content - assert_equal "ReadDocument", action.action_type - assert_equal({"document_url" => "https://example.com/survey"}, action.completion_criteria) - - # Execute the action - user = UserReadModel.first - SecureRandom.uuid - post "/actions/execute", params: { - action_id: action_id, - user_id: user.user_id, - timestamp: Time.current, - completion_data: {result: "success"} - } - assert_response :success - assert_equal 1, ActionLogReadModel.count - - action_log = ActionLogReadModel.last - assert_equal action_id, action_log.action_id - assert_equal user.user_id, action_log.user_id - assert_equal "Executed", action_log.status - assert_equal "success", action_log.completion_data["result"] - end -end diff --git a/apps/govquests-api/rails_app/test/integration/reward_claim_flow.rb b/apps/govquests-api/rails_app/test/integration/reward_claim_flow.rb deleted file mode 100644 index 2624814..0000000 --- a/apps/govquests-api/rails_app/test/integration/reward_claim_flow.rb +++ /dev/null @@ -1,47 +0,0 @@ -# rails_app/test/integration/reward_claim_flow_test.rb -require "test_helper" - -class RewardClaimFlowTest < IntegrationTest - test "issue and claim a reward" do - # Setup: Create user and reward - user_id = SecureRandom.uuid - post "/register", params: { - user_id: user_id, - email: "user3@example.com", - user_type: "non_delegate", - wallet_address: "0xA1B2C3D4E5F6G7H8", - chain_id: 1 - } - assert_response :success - - reward_id = SecureRandom.uuid - post "/rewards", params: { - reward_id: reward_id, - reward_type: "points", - value: 100, - expiry_date: (Time.current + 30.days).iso8601 - } - assert_response :success - assert_equal 1, RewardReadModel.count - - # Issue the reward to the user - post "/rewards/#{reward_id}/issue", params: {user_id: user_id} - assert_response :success - reward = RewardReadModel.find_by(reward_id: reward_id) - assert_not_nil reward - assert_equal "Issued", reward.delivery_status - assert_equal user_id, reward.issued_to - - # User claims the reward - post "/rewards/#{reward_id}/claim", params: {user_id: user_id} - assert_response :success - reward.reload - assert_equal true, reward.claimed - assert_equal "Claimed", reward.delivery_status - - # Attempt to claim again - post "/rewards/#{reward_id}/claim", params: {user_id: user_id} - assert_response :unprocessable_entity - assert_equal "Claimed", reward.delivery_status - end -end diff --git a/apps/govquests-api/rails_app/test/integration/user_auth_flow_test.rb b/apps/govquests-api/rails_app/test/integration/user_auth_flow_test.rb deleted file mode 100644 index 17ca1c9..0000000 --- a/apps/govquests-api/rails_app/test/integration/user_auth_flow_test.rb +++ /dev/null @@ -1,47 +0,0 @@ -# rails_app/test/integration/user_auth_flow_test.rb -require "test_helper" - -class UserAuthFlowTest < IntegrationTest - test "user registration and login" do - # Register a new user - post "/register", params: { - user_id: SecureRandom.uuid, - email: "user1@example.com", - user_type: "non_delegate", - wallet_address: "0xABCDEF1234567890", - chain_id: 1 - } - assert_response :success - assert_equal 1, UserReadModel.count - - user = UserReadModel.find_by(email: "user1@example.com") - assert_not_nil user - assert_equal "non_delegate", user.user_type - assert_equal "0xABCDEF1234567890", user.wallets.first["wallet_address"] - - # Attempt duplicate registration - post "/register", params: { - user_id: SecureRandom.uuid, - email: "user1@example.com", - user_type: "delegate", - wallet_address: "0x1234567890ABCDEF", - chain_id: 2 - } - assert_response :unprocessable_entity - assert_equal 1, UserReadModel.count # No new user created - - # Log in the user - post "/login", params: { - user_id: user.user_id, - session_token: SecureRandom.hex(16), - timestamp: Time.current - } - assert_response :success - assert_equal 1, SessionReadModel.count - - session = SessionReadModel.last - assert_equal user.user_id, session.user_id - assert_not_nil session.session_token - assert_nil session.logged_out_at - end -end diff --git a/apps/govquests-api/rails_app/test/integration/user_quest_reward_flow_test.rb b/apps/govquests-api/rails_app/test/integration/user_quest_reward_flow_test.rb deleted file mode 100644 index b916665..0000000 --- a/apps/govquests-api/rails_app/test/integration/user_quest_reward_flow_test.rb +++ /dev/null @@ -1,108 +0,0 @@ -# rails_app/test/integration/user_quest_reward_flow_test.rb -require "test_helper" - -class UserQuestRewardFlowTest < IntegrationTest - test "user completes quest and earns rewards" do - # Setup: Create user, actions, and quest - user_id = SecureRandom.uuid - post "/register", params: { - user_id: user_id, - email: "user2@example.com", - user_type: "delegate", - wallet_address: "0xFEDCBA0987654321", - chain_id: 1 - } - assert_response :success - - quest_id = SecureRandom.uuid - post "/quests", params: { - quest_id: quest_id, - title: "Governance 101", - intro: "Learn the basics of governance", - quest_type: "Onboarding", - audience: "AllUsers", - rewards: [{type: "points", amount: 50}] - } - assert_response :success - - action1_id = SecureRandom.uuid - post "/actions", params: { - action_id: action1_id, - content: "Read the Governance Policy", - action_type: "ReadDocument", - completion_criteria: {document_url: "https://example.com/governance"} - } - assert_response :success - - action2_id = SecureRandom.uuid - post "/actions", params: { - action_id: action2_id, - content: "Vote on Proposal", - action_type: "Vote", - completion_criteria: {proposal_id: "proposal_123"} - } - assert_response :success - - post "/quests/#{quest_id}/actions", params: { - action_id: action1_id, - position: 1 - } - assert_response :success - - post "/quests/#{quest_id}/actions", params: { - action_id: action2_id, - position: 2 - } - assert_response :success - - # User starts the quest - post "/quests/#{quest_id}/start", params: {user_id: user_id} - assert_response :success - assert_equal 1, UserQuest.count - - user_quest = UserQuest.find_by(quest_id: quest_id, user_id: user_id) - assert_not_nil user_quest - assert_equal "started", user_quest.status - - # User completes first action - post "/actions/execute", params: { - action_id: action1_id, - user_id: user_id, - timestamp: Time.current, - completion_data: {result: "success"} - } - assert_response :success - assert_equal 1, ActionLogReadModel.count - - # User completes second action - post "/actions/execute", params: { - action_id: action2_id, - user_id: user_id, - timestamp: Time.current, - completion_data: {result: "success"} - } - assert_response :success - assert_equal 2, ActionLogReadModel.count - - # Verify quest completion and reward issuance - # Assuming completing all actions in a quest triggers reward creation - # This might require observing emitted events or checking read models - - # Check GameProfile for earned points - game_profile = GameProfileReadModel.find_by(profile_id: user_id) - assert_not_nil game_profile - assert_equal 50, game_profile.score - - # Check Reward issuance - reward = RewardReadModel.find_by(issued_to: user_id) - assert_not_nil reward - assert_equal "points", reward.reward_type - assert_equal 50, reward.value - assert_equal "Issued", reward.delivery_status - - # Check Badge earning (if applicable) - # This depends on your gamification rules - # Example: - # assert_includes game_profile.badges, "First Quest Completed" - end -end