From dd681617ac9c0dece6af3f72268bb68df1f0346f Mon Sep 17 00:00:00 2001 From: Juliano Quatrin Nunes Date: Mon, 20 Jan 2025 16:13:08 -0300 Subject: [PATCH] feat: enable user to collect special badge --- .../gamification/lib/gamification.rb | 13 ++++++++ .../lib/gamification/special_badge.rb | 14 +++++---- .../base.rb | 0 .../certified_delegate.rb | 0 .../special_badge_strategy_factory.rb | 4 ++- ...eward_badge_on_quest_or_track_completed.rb | 2 +- .../app/graphql/mutations/collect_badge.rb | 8 ++--- .../gamification/on_badge_earned.rb | 4 ++- apps/govquests-api/rails_app/schema.graphql | 2 +- .../components/SpecialBadgeContent.tsx | 30 ++++++++++++++++++- .../gamification/graphql/collectBadge.ts | 10 +++++++ .../gamification/hooks/useCollectBadge.ts | 14 +++++++++ .../services/collectBadgeService.ts | 10 +++++++ .../domains/gamification/types/badgeTypes.ts | 6 +++- apps/govquests-frontend/src/graphql-env.d.ts | 2 +- 15 files changed, 102 insertions(+), 17 deletions(-) rename apps/govquests-api/govquests/gamification/lib/gamification/{special_badge_strategies => strategies}/base.rb (100%) rename apps/govquests-api/govquests/gamification/lib/gamification/{special_badge_strategies => strategies}/certified_delegate.rb (100%) rename apps/govquests-api/govquests/gamification/lib/gamification/{special_badge_strategies => strategies}/special_badge_strategy_factory.rb (73%) create mode 100644 apps/govquests-frontend/src/domains/gamification/graphql/collectBadge.ts create mode 100644 apps/govquests-frontend/src/domains/gamification/hooks/useCollectBadge.ts create mode 100644 apps/govquests-frontend/src/domains/gamification/services/collectBadgeService.ts diff --git a/apps/govquests-api/govquests/gamification/lib/gamification.rb b/apps/govquests-api/govquests/gamification/lib/gamification.rb index e92201df..e3962b15 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification.rb @@ -7,6 +7,17 @@ require_relative "gamification/user_badge" require_relative "gamification/special_badge" +require_relative "gamification/strategies/base" +require_relative "gamification/strategies/special_badge_strategy_factory" + +Dir[File.join(__dir__, "gamification/strategies/*.rb")].each do |f| + next if f.end_with?("base.rb") + next if f.end_with?("special_badge_strategy_factory.rb") + require_relative f +end + +puts "Available constants in Gamification::Strategies: #{Gamification::Strategies.constants}" + ACTION_BADGE_NAMESPACE_UUID = "5FA78373-03E0-4D0B-91D1-3F2C6CA3F088" module Gamification @@ -23,6 +34,8 @@ def generate_badge_id(entity_name, entity_id) class Configuration def call(event_store, command_bus) CommandHandler.register_commands(event_store, command_bus) + + Gamification.command_bus = command_bus end end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/special_badge.rb b/apps/govquests-api/govquests/gamification/lib/gamification/special_badge.rb index 15551e5e..a097184c 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification/special_badge.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification/special_badge.rb @@ -34,14 +34,16 @@ def verify_completion?(user_id:) strategy.verify_completion? end - def collect_badge(user_id:) + def collect_badge(user_id) raise VerificationFailedError unless verify_completion?(user_id: user_id) - apply EarnBadge.new(data: { - user_id:, - badge_id: @id, - badge_type: "Gamification::SpecialBadgeReadModel", - }) + Gamification.command_bus.call( + EarnBadge.new( + user_id: user_id, + badge_id: @id, + badge_type: "Gamification::SpecialBadgeReadModel" + ) + ) end private diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/special_badge_strategies/base.rb b/apps/govquests-api/govquests/gamification/lib/gamification/strategies/base.rb similarity index 100% rename from apps/govquests-api/govquests/gamification/lib/gamification/special_badge_strategies/base.rb rename to apps/govquests-api/govquests/gamification/lib/gamification/strategies/base.rb diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/special_badge_strategies/certified_delegate.rb b/apps/govquests-api/govquests/gamification/lib/gamification/strategies/certified_delegate.rb similarity index 100% rename from apps/govquests-api/govquests/gamification/lib/gamification/special_badge_strategies/certified_delegate.rb rename to apps/govquests-api/govquests/gamification/lib/gamification/strategies/certified_delegate.rb diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/special_badge_strategies/special_badge_strategy_factory.rb b/apps/govquests-api/govquests/gamification/lib/gamification/strategies/special_badge_strategy_factory.rb similarity index 73% rename from apps/govquests-api/govquests/gamification/lib/gamification/special_badge_strategies/special_badge_strategy_factory.rb rename to apps/govquests-api/govquests/gamification/lib/gamification/strategies/special_badge_strategy_factory.rb index baf0bd71..f84181a1 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification/special_badge_strategies/special_badge_strategy_factory.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification/strategies/special_badge_strategy_factory.rb @@ -9,7 +9,9 @@ def strategies def for(badge_type, **dependencies) strategy_name = badge_type.to_s.camelize - strategy_class = "Gamification::SpecialBadgeStrategies::#{strategy_name}".constantize + strategy_class = "Gamification::Strategies::#{strategy_name}".constantize + + puts "Looking for class: Gamification::Strategies::#{strategy_name}" # Debug strategy_class.new(**dependencies) rescue NameError diff --git a/apps/govquests-api/govquests/processes/lib/processes/reward_badge_on_quest_or_track_completed.rb b/apps/govquests-api/govquests/processes/lib/processes/reward_badge_on_quest_or_track_completed.rb index e7fe52c9..c4e85209 100644 --- a/apps/govquests-api/govquests/processes/lib/processes/reward_badge_on_quest_or_track_completed.rb +++ b/apps/govquests-api/govquests/processes/lib/processes/reward_badge_on_quest_or_track_completed.rb @@ -31,7 +31,7 @@ def call(event) @command_bus.call( ::Gamification::EarnBadge.new( user_id:, - badge_id: badge.id.to_s, + badge_id: badge.badge_id, badge_type: badge.class.name, ) ) diff --git a/apps/govquests-api/rails_app/app/graphql/mutations/collect_badge.rb b/apps/govquests-api/rails_app/app/graphql/mutations/collect_badge.rb index e030786d..9b225f7f 100644 --- a/apps/govquests-api/rails_app/app/graphql/mutations/collect_badge.rb +++ b/apps/govquests-api/rails_app/app/graphql/mutations/collect_badge.rb @@ -1,22 +1,22 @@ module Mutations class CollectBadge < BaseMutation argument :badge_id, ID, required: true - argument :badge_type, ID, required: true, description: "Type of the badge" + argument :badge_type, String, required: true, description: "Type of the badge" field :badge_earned, Boolean, null: true field :errors, [String], null: false def resolve(badge_id:, badge_type:) - badge = Gamification::BadgeReadModel.find_by(badge_id: badge_id) + badge = Gamification::SpecialBadgeReadModel.find_by(badge_id: badge_id) unless badge return {badge_earned: false, errors: ["Badge not found"]} end - command_bus.call( + Rails.configuration.command_bus.call( Gamification::CollectSpecialBadge.new( badge_id: badge_id, - user_id: context[:current_user].id + user_id: context[:current_user].user_id ) ) 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 40fc0e66..73c7cc75 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 @@ -6,9 +6,11 @@ def call(event) badge_type = event.data[:badge_type] earned_at = event.data[:earned_at] + badge = badge_type.constantize.find_by(badge_id: badge_id) + UserBadgeReadModel.create!( user_id:, - badge_id: badge_id, + badge_id: badge.id, badge_type: badge_type, earned_at: earned_at ) diff --git a/apps/govquests-api/rails_app/schema.graphql b/apps/govquests-api/rails_app/schema.graphql index 3010662e..0c47f37f 100644 --- a/apps/govquests-api/rails_app/schema.graphql +++ b/apps/govquests-api/rails_app/schema.graphql @@ -80,7 +80,7 @@ input CollectBadgeInput { """ Type of the badge """ - badgeType: ID! + badgeType: String! """ A unique identifier for the client performing the mutation. diff --git a/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgeContent.tsx b/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgeContent.tsx index da9c72d9..40dfbef4 100644 --- a/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgeContent.tsx +++ b/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgeContent.tsx @@ -1,14 +1,38 @@ import { Button } from "@/components/ui/Button"; import { useState } from "react"; import { useFetchSpecialBadge } from "../hooks/useFetchSpecialBadge"; +import { useCollectBadge } from "../hooks/useCollectBadge"; export const SpecialBadgeContent = ({ badgeId }: { badgeId: string }) => { const { data } = useFetchSpecialBadge(badgeId); const isCompleted = data.specialBadge.earnedByCurrentUser; + const { mutate } = useCollectBadge(); + const [error, setError] = useState(null); + const handleCollectBadge = () => { + mutate( + { + badgeId, + badgeType: data.specialBadge.badgeType, + }, + { + onSuccess: (result) => { + if (result.collectBadge.errors) { + setError(result.collectBadge.errors); + } else { + setError(null); + } + }, + onError: (error) => { + setError(error.message); + }, + }, + ); + }; + return ( data && (
@@ -27,7 +51,11 @@ export const SpecialBadgeContent = ({ badgeId }: { badgeId: string }) => { now.
- {error && ( diff --git a/apps/govquests-frontend/src/domains/gamification/graphql/collectBadge.ts b/apps/govquests-frontend/src/domains/gamification/graphql/collectBadge.ts new file mode 100644 index 00000000..f67311a3 --- /dev/null +++ b/apps/govquests-frontend/src/domains/gamification/graphql/collectBadge.ts @@ -0,0 +1,10 @@ +import { graphql } from "gql.tada"; + +export const COLLECT_BADGE = graphql(` + mutation CollectBadge($badgeId: ID!, $badgeType: String!) { + collectBadge(input: { badgeId: $badgeId, badgeType: $badgeType }) { + badgeEarned + errors + } + } +`); diff --git a/apps/govquests-frontend/src/domains/gamification/hooks/useCollectBadge.ts b/apps/govquests-frontend/src/domains/gamification/hooks/useCollectBadge.ts new file mode 100644 index 00000000..1081180f --- /dev/null +++ b/apps/govquests-frontend/src/domains/gamification/hooks/useCollectBadge.ts @@ -0,0 +1,14 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { collectBadge } from "../services/collectBadgeService"; +import { CollectBadgeResult, CollectBadgeVariables } from "../types/badgeTypes"; + +export const useCollectBadge = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: collectBadge, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["specialBadge"] }); + queryClient.invalidateQueries({ queryKey: ["specialBadges"] }); + }, + }); +}; diff --git a/apps/govquests-frontend/src/domains/gamification/services/collectBadgeService.ts b/apps/govquests-frontend/src/domains/gamification/services/collectBadgeService.ts new file mode 100644 index 00000000..5a54b7de --- /dev/null +++ b/apps/govquests-frontend/src/domains/gamification/services/collectBadgeService.ts @@ -0,0 +1,10 @@ +import { API_URL } from "@/lib/utils"; +import request from "graphql-request"; +import { COLLECT_BADGE } from "../graphql/collectBadge"; +import { CollectBadgeResult, CollectBadgeVariables } from "../types/badgeTypes"; + +export const collectBadge = async ( + variables: CollectBadgeVariables, +): Promise => { + return await request(API_URL, COLLECT_BADGE, variables); +}; diff --git a/apps/govquests-frontend/src/domains/gamification/types/badgeTypes.ts b/apps/govquests-frontend/src/domains/gamification/types/badgeTypes.ts index df4c0d88..a0fcf8a9 100644 --- a/apps/govquests-frontend/src/domains/gamification/types/badgeTypes.ts +++ b/apps/govquests-frontend/src/domains/gamification/types/badgeTypes.ts @@ -1,4 +1,8 @@ -import { ResultOf } from "gql.tada"; +import { ResultOf, VariablesOf } from "gql.tada"; import { BadgeQuery } from "../graphql/badgeQuery"; +import { COLLECT_BADGE } from "../graphql/collectBadge"; export type Badge = ResultOf["badge"]; + +export type CollectBadgeVariables = VariablesOf; +export type CollectBadgeResult = ResultOf; diff --git a/apps/govquests-frontend/src/graphql-env.d.ts b/apps/govquests-frontend/src/graphql-env.d.ts index 4b4db92d..66f5687b 100644 --- a/apps/govquests-frontend/src/graphql-env.d.ts +++ b/apps/govquests-frontend/src/graphql-env.d.ts @@ -11,7 +11,7 @@ export type introspection_types = { 'BadgeDisplayData': { kind: 'OBJECT'; name: 'BadgeDisplayData'; fields: { 'description': { name: 'description'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'imageUrl': { name: 'imageUrl'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'sequenceNumber': { name: 'sequenceNumber'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'title': { name: 'title'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 'BadgeableUnion': { kind: 'UNION'; name: 'BadgeableUnion'; fields: {}; possibleTypes: 'Quest' | 'Track'; }; 'Boolean': unknown; - 'CollectBadgeInput': { kind: 'INPUT_OBJECT'; name: 'CollectBadgeInput'; isOneOf: false; inputFields: [{ name: 'badgeId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'badgeType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; + 'CollectBadgeInput': { kind: 'INPUT_OBJECT'; name: 'CollectBadgeInput'; isOneOf: false; inputFields: [{ name: 'badgeId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'badgeType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; 'CollectBadgePayload': { kind: 'OBJECT'; name: 'CollectBadgePayload'; fields: { 'badgeEarned': { name: 'badgeEarned'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'clientMutationId': { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'errors': { name: 'errors'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; }; }; 'CompleteActionExecutionInput': { kind: 'INPUT_OBJECT'; name: 'CompleteActionExecutionInput'; isOneOf: false; inputFields: [{ name: 'actionType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'discourseVerificationCompletionData'; type: { kind: 'INPUT_OBJECT'; name: 'DiscourseVerificationCompletionDataInput'; ofType: null; }; defaultValue: null }, { name: 'executionId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'gitcoinScoreCompletionData'; type: { kind: 'INPUT_OBJECT'; name: 'GitcoinScoreCompletionDataInput'; ofType: null; }; defaultValue: null }, { name: 'nonce'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }]; }; 'CompleteActionExecutionPayload': { kind: 'OBJECT'; name: 'CompleteActionExecutionPayload'; fields: { 'actionExecution': { name: 'actionExecution'; type: { kind: 'OBJECT'; name: 'ActionExecution'; ofType: null; } }; 'clientMutationId': { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'errors': { name: 'errors'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; }; };