Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Juliano/op 677 add user badge #132

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/govquests-api/govquests/gamification/lib/gamification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require_relative "gamification/game_profile"
require_relative "gamification/leaderboard"
require_relative "gamification/badge"
require_relative "gamification/user_badge"

ACTION_BADGE_NAMESPACE_UUID = "5FA78373-03E0-4D0B-91D1-3F2C6CA3F088"

Expand Down Expand Up @@ -77,5 +78,9 @@ class CommandHandler < Infra::CommandHandlerRegistry
handle "Gamification::CreateBadge", aggregate: Badge do |badge, cmd|
badge.create(cmd.display_data, cmd.badgeable_id, cmd.badgeable_type)
end

handle "Gamification::EarnBadge", aggregate: UserBadge do |user_badge, cmd|
user_badge.earn_badge(cmd.user_id, cmd.badgeable_id, cmd.badgeable_type, cmd.earned_at)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,6 @@ class MaintainStreak < Infra::Command
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
Expand All @@ -76,4 +69,13 @@ class CreateBadge < Infra::Command

alias_method :aggregate_id, :badge_id
end

class EarnBadge < Infra::Command
attribute :user_id, Infra::Types::UUID
attribute :badgeable_id, Infra::Types::String
attribute :badgeable_type, Infra::Types::String
attribute :earned_at, Infra::Types::DateTime.default { Time.current.to_datetime }

alias_method :aggregate_id, :user_id
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,7 @@ 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
Expand All @@ -64,4 +59,11 @@ class BadgeCreated < Infra::Event
attribute :badgeable_id, Infra::Types::UUID
attribute :badgeable_type, Infra::Types::String
end

class BadgeEarned < Infra::Event
attribute :user_id, Infra::Types::UUID
attribute :badgeable_id, Infra::Types::String
attribute :badgeable_type, Infra::Types::String
attribute :earned_at, Infra::Types::DateTime
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Gamification
class UserBadge
include AggregateRoot

def initialize(id)
@id = id
end

def earn_badge(user_id, badgeable_id, badgeable_type, earned_at)
apply BadgeEarned.new(data: {
user_id:,
badgeable_id:,
badgeable_type:,
earned_at:,
})
end

private

on BadgeEarned do |event|
@user_id = event.data[:user_id]
@badgeable_id = event.data[:badgeable_id]
@badgeable_type = event.data[:badgeable_type]
@earned_at = event.data[:earned_at]
end
end
end
6 changes: 5 additions & 1 deletion apps/govquests-api/govquests/processes/lib/processes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
require_relative "processes/notify_on_badge_earned"
require_relative "processes/notify_on_tier_achieved"
require_relative "processes/create_badge_on_track_or_quest_created"
require_relative "processes/reward_badge_on_quest_or_track_completed"
require_relative "processes/complete_track_on_quest_completed"

require_relative "processes/deliver_notification_on_created"
module Processes
Expand All @@ -20,13 +22,15 @@ def call(event_store, command_bus)
UpdateProfileOnRewardIssued.new(event_store, command_bus).subscribe
DistributeRewardsOnQuestCompleted.new(event_store, command_bus).subscribe
CreateBadgeOnTrackOrQuestCreated.new(event_store, command_bus).subscribe
RewardBadgeOnQuestOrTrackCompleted.new(event_store, command_bus).subscribe
CompleteTrackOnQuestCompleted.new(event_store, command_bus).subscribe

NotifyOnQuestCompleted.new(event_store, command_bus).subscribe
NotifyOnRewardIssued.new(event_store, command_bus).subscribe
NotifyOnBadgeEarned.new(event_store, command_bus).subscribe
NotifyOnTierAchieved.new(event_store, command_bus).subscribe

DeliverNotificationOnCreated.new(event_store, command_bus).subscribe
DeliverNotificationOnCreated.new(event_store, command_bus).subscribe
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module Processes
class CompleteTrackOnQuestCompleted
def initialize(event_store, command_bus)
@event_store = event_store
@command_bus = command_bus
end

def subscribe
@event_store.subscribe(self, to: [Questing::QuestCompleted])
end

def call(event)
quest = Questing::QuestReadModel.find_by(quest_id: event.data[:quest_id])
return unless quest&.track_id

track = quest.track
user_id = event.data[:user_id]

completed_quests_count = track.quests
.joins(:user_quests)
.where(
user_quests: {
user_id: user_id,
status: "completed"
}
).count

if completed_quests_count == track.quests.count
@command_bus.call(
Questing::CompleteTrack.new(
user_id: user_id,
track_id: track.track_id
)
)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,27 @@ def subscribe
end

def call(event)
display_data = event.data[:badge_display_data]

entity_type = event.class.name.split('::').last.gsub('Created', '')
entity_id = event.data["#{entity_type.downcase}_id".to_sym]
source_name = event.class.name.split('::').last.gsub('Created', '')
source_type = "::Questing::#{source_name}ReadModel"

badge_id = Gamification.generate_badge_id(entity_type, entity_id)
source_id_field = "#{source_name.downcase}_id".to_sym
source_uuid = event.data[source_id_field]

source_sequential_id = source_type.constantize.find_by(source_id_field => source_uuid).id

display_data = event.data[:badge_display_data].merge({
sequence_number: source_sequential_id
})

badge_id = Gamification.generate_badge_id(source_type, source_uuid)

@command_bus.call(
::Gamification::CreateBadge.new(
badge_id:,
display_data:,
badgeable_id: entity_id,
badgeable_type: entity_type
badgeable_id: source_uuid,
badgeable_type: source_name
)
)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ def subscribe
end

def call(event)
profile_id = event.data[:profile_id]
badge = event.data[:badge]
user_id = event.data[:user_id]
badgeable_id = event.data[:badgeable_id]
badgeable_type = event.data[:badgeable_type]

badge_record = badgeable_type.constantize.find_by(id: badgeable_id)
badge_title = badge_record&.display_data&.dig("title") if badge_record

@command_bus.call(
::Notifications::CreateNotification.new(
notification_id: SecureRandom.uuid,
user_id: profile_id,
content: "Congratulations! You've earned the #{badge} badge!",
user_id: ,
content: "Congratulations! You've earned the #{badge_title} badge!",
notification_type: "badge_earned"
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module Processes
class RewardBadgeOnQuestOrTrackCompleted
def initialize(event_store, command_bus)
@event_store = event_store
@command_bus = command_bus
end

def subscribe
@event_store.subscribe(self, to: [::Questing::QuestCompleted, ::Questing::TrackCompleted])
end

def call(event)
user_id = event.data[:user_id]

entity_name = event.class.name.split('::').last.gsub('Completed', '')
entity_type = case entity_name
when 'Quest' then 'Questing::QuestReadModel'
when 'Track' then 'Questing::TrackReadModel'
end
entity_id = event.data["#{entity_name.downcase}_id".to_sym]

entity = entity_type.constantize.find_by("#{entity_name.downcase}_id": entity_id)

badge = Gamification::BadgeReadModel.find_by(
badgeable_type: entity_type,
badgeable_id: entity.id.to_s,
)

return unless badge

@command_bus.call(
::Gamification::EarnBadge.new(
user_id:,
badgeable_id: badge.id.to_s,
badgeable_type: badge.class.name,
)
)
end
end
end
4 changes: 4 additions & 0 deletions apps/govquests-api/govquests/questing/lib/questing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,9 @@ class CommandHandler < Infra::CommandHandlerRegistry
badge_display_data: cmd.badge_display_data
)
end

handle "Questing::CompleteTrack", aggregate: Track do |track, cmd|
track.complete(user_id: cmd.user_id)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,11 @@ class CreateTrack < Infra::Command

alias_method :aggregate_id, :track_id
end

class CompleteTrack < Infra::Command
attribute :user_id, Infra::Types::UUID
attribute :track_id, Infra::Types::UUID

alias_method :aggregate_id, :track_id
end
end
5 changes: 5 additions & 0 deletions apps/govquests-api/govquests/questing/lib/questing/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,9 @@ class TrackCreated < Infra::Event
attribute :quest_ids, Infra::Types::Array
attribute :badge_display_data, Infra::Types::Hash
end

class TrackCompleted < Infra::Event
attribute :user_id, Infra::Types::UUID
attribute :track_id, Infra::Types::UUID
end
end
10 changes: 10 additions & 0 deletions apps/govquests-api/govquests/questing/lib/questing/track.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,20 @@ def create(display_data:, quest_ids:, badge_display_data:)
)
end

def complete(user_id:)
apply(TrackCompleted.new(data: {
user_id:,
track_id: @id
}))
end

on TrackCreated do |event|
@display_data = event.data[:display_data]
@quest_ids = event.data[:quest_ids]
@badge_display_data = event.data[:badge_display_data]
end

on TrackCompleted do |event|
end
end
end
27 changes: 17 additions & 10 deletions apps/govquests-api/rails_app/app/graphql/resolvers/fetch_badges.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ module Resolvers
class FetchBadges < BaseResolver
type [Types::BadgeType], null: true

argument :special, Boolean, required: false, default_value: nil
def resolve
current_user = context[:current_user]
badges = Gamification::BadgeReadModel.includes(:user_badges)

def resolve(special:)
case special
when true
Gamification::BadgeReadModel.special
when false
Gamification::BadgeReadModel.normal
else
Gamification::BadgeReadModel.all
end
return badges unless current_user

earned_badge_ids = Gamification::UserBadgeReadModel
.where(user_id: current_user.user_id)
.where(badgeable_type: "Gamification::BadgeReadModel")
.pluck(:badgeable_id)

return badges if earned_badge_ids.empty?

badges.order(
Arel.sql(
"CASE WHEN badges.id IN (#{earned_badge_ids.join(",")}) THEN 0 ELSE 1 END"
)
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Resolvers
class FetchSpecialBadge < BaseResolver
type Types::SpecialBadgeType, null: true

argument :id, ID, required: true

def resolve(id:)
Gamification::SpecialBadgeReadModel.find_by(badge_id: id)
ribeirojose marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Resolvers
class FetchSpecialBadges < BaseResolver
type [Types::SpecialBadgeType], null: true

def resolve
current_user = context[:current_user]
badges = Gamification::SpecialBadgeReadModel.includes(:user_badges)

return badges unless current_user

earned_badge_ids = Gamification::UserBadgeReadModel
.where(user_id: current_user.user_id)
.where(badgeable_type: "Gamification::SpecialBadgeReadModel")
.pluck(:badgeable_id)

return badges if earned_badge_ids.empty?

badges.order(
Arel.sql(
"CASE WHEN special_badges.id IN (#{earned_badge_ids.join(",")}) THEN 0 ELSE 1 END"
)
)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ class BadgeDisplayDataType < Types::BaseObject
field :title, String, null: true
field :description, String, null: true
field :image_url, String, null: true
field :sequence_number, Integer, null: true
end
end
Loading
Loading