-
Notifications
You must be signed in to change notification settings - Fork 13
Description
A He4rt Developers está construindo um sistema econômico que recompensa contribuições da comunidade com coins. O primeiro passo é o Activity Tracking — a base que captura, classifica e roteia atividades pra serem recompensadas.
Problema atual: O módulo activity/ rastreia apenas mensagens e voice do Discord, com estrutura flat e sem modelo unificado de atividades. Não há classificação por valor, status de aprovação, nem suporte a múltiplos providers.
Objetivo: Reestruturar o módulo activity/ com sub-domínios, criar o modelo Interaction como registro unificado de atividades, e implementar DevTo como primeiro provider externo (artigos + engagement snapshot).
Escopo MVP: Apenas DevTo como provider. Discord adapter e outros ficam pra depois.
┌─────────────────────────────────────────────────────────────────┐
│ ARQUITETURA ALVO │
│ │
│ integration-devto/ activity/ │
│ ┌─────────────┐ DTO ┌──────────────┐ Economy │
│ │ SyncArticles│──────────→│TrackActivity │──────────→ Credit │
│ │ (scheduled) │ │(entry point) │ │
│ └─────────────┘ └──────┬───────┘ │
│ │ │ │
│ │ OAuth │ ClassifyActivity │
│ ▼ ▼ │
│ identity/ ┌───────────────┐ │
│ ┌────────────┐ │ Interaction │ │
│ │ExternalId │←─────────│ (persisted) │ │
│ │(DevTo user)│ └───────────────┘ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Step 1: Reestruturar módulo activity/ em sub-domínios
Context
O módulo activity/ tem todos os arquivos flat em src/ (Actions, DTOs, Models, Http). Precisa ser reorganizado em sub-domínios Message/, Voice/ e Tracking/ para acomodar o novo pipeline. Os arquivos a mover:
Message/ (mover de src/):
src/Models/Message.php→src/Message/Models/Message.phpsrc/Actions/NewMessage.php→src/Message/Actions/NewMessage.phpsrc/Actions/PersistMessage.php→src/Message/Actions/PersistMessage.phpsrc/DTOs/NewMessageDTO.php→src/Message/DTOs/NewMessageDTO.phpsrc/Http/Controllers/MessagesController.php→src/Message/Http/Controllers/MessagesController.phpsrc/Http/Requests/CreateMessageRequest.php→src/Message/Http/Requests/CreateMessageRequest.phpsrc/Http/Requests/CreateVoiceMessageRequest.php→src/Voice/Http/Requests/CreateVoiceMessageRequest.phpsrc/Filament/Admin/Resources/Messages/*→src/Message/Filament/Admin/Resources/Messages/*
Voice/ (mover de src/):
src/Models/Voice.php→src/Voice/Models/Voice.phpsrc/Actions/NewVoiceMessage.php→src/Voice/Actions/NewVoiceMessage.phpsrc/DTOs/NewVoiceMessageDTO.php→src/Voice/DTOs/NewVoiceMessageDTO.php
Namespaces afetados:
He4rt\Activity\Models\Message→He4rt\Activity\Message\Models\MessageHe4rt\Activity\Models\Voice→He4rt\Activity\Voice\Models\VoiceHe4rt\Activity\Actions\*→He4rt\Activity\Message\Actions\*/He4rt\Activity\Voice\Actions\*He4rt\Activity\DTOs\*→He4rt\Activity\Message\DTOs\*/He4rt\Activity\Voice\DTOs\*
Arquivos que referenciam os namespaces antigos (atualizar):
app-modules/activity/routes/message-routes.phpapp-modules/activity/src/Providers/ActivityServiceProvider.phpapp-modules/activity/database/factories/MessageFactory.phpapp-modules/bot-discord/src/Events/MessageReceivedEvent.php(importa NewMessage/NewMessageDTO)app-modules/community/(se referencia Message model)- Todos os testes em
app-modules/activity/tests/
Expected Behavior
Dado que o módulo activity tem arquivos flat em src/
Quando eu reorganizo em sub-domínios Message/, Voice/
Então todos os namespaces são atualizados para He4rt\Activity\{Message,Voice}\*
E composer dump-autoload executa sem erros
E todos os testes existentes passam com os novos namespaces
E o MessageReceivedEvent do bot-discord usa o novo namespace
E as rotas POST /api/messages/{provider} e POST /api/voices/{provider} continuam funcionando
E o Filament admin MessageResource renderiza corretamenteStep 2: Criar sub-domínio Tracking/ com modelo Interaction
Context
O sub-domínio Tracking/ é o core do Activity Tracking. O modelo Interaction é o registro unificado de qualquer atividade rastreada, com classificação por valor, status de aprovação, e referência polimórfica à fonte.
Criar em app-modules/activity/src/Tracking/:
Tracking/
Models/
Interaction.php
Actions/
TrackActivity.php ← ponto de entrada único
ClassifyActivity.php ← determina tier + coin range
CalculateReward.php ← formula: base + engagement bonus
ApproveInteraction.php ← aprovação manual (interim)
RejectInteraction.php
DTOs/
TrackActivityDTO.php
Enums/
ActivityType.php ← article, pr_merged, mentoring, message, voice...
ActivityStatus.php ← pending, auto_approved, in_review, approved, rejected
ValueTier.php ← high, medium, low
Contracts/
ActivitySourceContract.php ← interface que cada provider implementa
Events/
InteractionTracked.php
InteractionApproved.php
Concerns/
HasInteractions.php ← trait pro Character model
Migration create_interactions_table:
CREATE TABLE interactions (
id UUID PRIMARY KEY,
character_id UUID NOT NULL REFERENCES characters(id),
tenant_id UUID NOT NULL REFERENCES tenants(id),
type VARCHAR NOT NULL, -- ActivityType enum
provider VARCHAR NOT NULL, -- IdentityProvider enum
value_tier VARCHAR NOT NULL, -- ValueTier enum
coins_min INTEGER NOT NULL,
coins_max INTEGER NOT NULL,
coins_awarded INTEGER NULL, -- set after approval
xp_awarded INTEGER NULL, -- set after approval
status VARCHAR NOT NULL DEFAULT 'pending',
source_type VARCHAR NULL, -- polymorphic
source_id UUID NULL, -- polymorphic
external_ref VARCHAR NULL UNIQUE, -- deduplication key
metadata JSONB NULL, -- provider-specific data
occurred_at TIMESTAMP NOT NULL,
reviewed_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_interactions_character_type ON interactions(character_id, type, created_at);
CREATE INDEX idx_interactions_status_tier ON interactions(status, value_tier);
CREATE INDEX idx_interactions_tenant ON interactions(tenant_id, occurred_at);
CREATE INDEX idx_interactions_source ON interactions(source_type, source_id);ActivityType enum — valores iniciais:
enum ActivityType: string {
case Article = 'article';
case PrMerged = 'pr_merged';
case Mentoring = 'mentoring';
case SquadProject = 'squad_project';
case Referral = 'referral';
case PeerReview = 'peer_review';
case CallParticipation = 'call_participation';
case ForumDebate = 'forum_debate';
case ContentShare = 'content_share';
case Engagement = 'engagement';
case RepoStar = 'repo_star';
case Message = 'message';
case Voice = 'voice';
}Tabela de classificação (config-driven, não hardcoded):
// config/activity-tracking.php
'classification' => [
'article' => ['tier' => 'high', 'coins_min' => 100, 'coins_max' => 300],
'pr_merged' => ['tier' => 'high', 'coins_min' => 80, 'coins_max' => 250],
'mentoring' => ['tier' => 'high', 'coins_min' => 50, 'coins_max' => 150],
'squad_project' => ['tier' => 'high', 'coins_min' => 200, 'coins_max' => 500],
'referral' => ['tier' => 'medium', 'coins_min' => 20, 'coins_max' => 30],
'peer_review' => ['tier' => 'medium', 'coins_min' => 10, 'coins_max' => 25],
'call_participation' => ['tier' => 'medium', 'coins_min' => 15, 'coins_max' => 30],
'forum_debate' => ['tier' => 'medium', 'coins_min' => 10, 'coins_max' => 20],
'content_share' => ['tier' => 'low', 'coins_min' => 5, 'coins_max' => 10],
'engagement' => ['tier' => 'low', 'coins_min' => 1, 'coins_max' => 3],
'repo_star' => ['tier' => 'low', 'coins_min' => 2, 'coins_max' => 2],
],
'auto_approve_tiers' => ['low', 'medium'],
'engagement_formula' => [
'reactions_multiplier' => 0.5,
'reactions_cap' => 25,
'bookmarks_multiplier' => 1.0,
'bookmarks_cap' => 15,
'comments_multiplier' => 2.0,
'comments_cap' => 30,
],TrackActivity Action (ponto de entrada):
1. Recebe TrackActivityDTO
2. Verifica dedup via external_ref (se já existe, skip)
3. Resolve Character via character_id ou ExternalIdentity
4. Chama ClassifyActivity → retorna tier, coins_min, coins_max, status
5. Persiste Interaction
6. Se status == auto_approved → chama CalculateReward + Economy::Credit
7. Dispara InteractionTracked event
CalculateReward Action:
1. Recebe Interaction
2. Se tem metadata.engagement_snapshot:
base = peer_review_score (ou midpoint se auto-approved)
bonus = reactions * mult + bookmarks * mult + comments * mult (com caps)
coins_awarded = min(base + bonus, coins_max)
3. Se não tem engagement:
coins_awarded = para auto_approved → coins_min
para peer_reviewed → baseado no score
4. xp_awarded = coins_awarded * XP_MULTIPLIER (config)
5. Atualiza Interaction com coins_awarded e xp_awarded
Expected Behavior
Dado um TrackActivityDTO válido com type='article' e provider='devto'
Quando TrackActivity é chamado
Então uma nova Interaction é criada com status 'pending' (high tier)
E value_tier é 'high', coins_min é 100, coins_max é 300
E o evento InteractionTracked é disparado
E Economy::Credit NÃO é chamado (high tier precisa de review)
Dado um TrackActivityDTO válido com type='engagement' e provider='devto'
Quando TrackActivity é chamado
Então uma nova Interaction é criada com status 'auto_approved' (low tier)
E CalculateReward é chamado
E Economy::Credit é chamado com coins_min (1) pra wallet do character
E o evento InteractionTracked é disparado
Dado um TrackActivityDTO com external_ref que já existe no banco
Quando TrackActivity é chamado
Então nenhuma nova Interaction é criada (deduplicação)
E nenhuma exceção é lançada (idempotente)
Dado uma Interaction aprovada com engagement_snapshot {reactions: 42, bookmarks: 8, comments: 12}
Quando CalculateReward é chamado com peer_review_base = 200
Então engagement_bonus = min(42*0.5, 25) + min(8*1.0, 15) + min(12*2.0, 30) = 21 + 8 + 24 = 53
E coins_awarded = min(200 + 53, 300) = 253
E xp_awarded = 253 * XP_MULTIPLIERStep 3: Criar módulo integration-devto/
Context
O DevTo é o primeiro provider externo. Segue o padrão existente de integration-discord/ e integration-twitch/: módulo separado com OAuth client, API client, e lógica de polling. O módulo não contém lógica de tracking — apenas normaliza dados do DevTo e os empurra pro TrackActivity do módulo activity/.
Estrutura:
app-modules/integration-devto/
src/
OAuth/
DevToOAuthClient.php ← implements OAuthClientContract
DevToOAuthAccessDTO.php ← extends OAuthAccessDTO
DevToOAuthUser.php ← extends OAuthUserDTO
Polling/
SyncDevToArticles.php ← artisan command (scheduled)
DevToApiClient.php ← wrapper HTTP para DevTo API
Providers/
IntegrationDevToServiceProvider.php
config/
integration-devto.php
tests/
Feature/
DevToOAuthTest.php
SyncDevToArticlesTest.php
composer.json
DevToApiClient — endpoints usados:
GET /api/articles?username={org_slug}&per_page=30&page={page}— artigos da orgGET /api/articles/{id}— detalhes do artigo (engagement metrics)
SyncDevToArticles Command:
1. Busca artigos da org He4rt no DevTo (paginado)
2. Para cada artigo:
a. Verifica se autor tem ExternalIdentity DevTo vinculada
b. Se não tem → log e skip
c. Se já tem Interaction com external_ref "devto:article:{id}" → atualiza engagement no metadata
d. Se é novo → cria TrackActivityDTO e chama TrackActivity
3. Roda via scheduler a cada 30 min (configurável)
Config (config/integration-devto.php):
return [
'org_slug' => env('DEVTO_ORG_SLUG', 'he4rt'),
'api_base_url' => env('DEVTO_API_URL', 'https://dev.to/api'),
'polling_interval_minutes' => env('DEVTO_POLLING_INTERVAL', 30),
];DevTo OAuth (Forem API):
- Base URL:
https://dev.to/oauth/authorize - Token URL:
https://dev.to/oauth/token - User URL:
https://dev.to/api/users/me - Scopes:
public
Implementar seguindo o padrão de integration-discord/:
DevToOAuthClientimplementaOAuthClientContractredirectUrl(?OAuthStateDTO)→ redireciona pro DevTo OAuthauth(string $code)→ troca code por tokengetAuthenticatedUser(OAuthAccessDTO)→ busca user info
Expected Behavior
Dado que a org "he4rt" no DevTo tem 5 artigos publicados
E 3 dos autores têm ExternalIdentity DevTo vinculada
Quando php artisan devto:sync-articles executa
Então 3 Interactions são criadas (uma por autor vinculado)
E 2 artigos são logados como "autor não vinculado" e ignorados
E cada Interaction tem external_ref "devto:article:{id}"
E metadata contém devto_article_id, title, url, tags, engagement_snapshot
Dado que o artigo "devto:article:123" já tem uma Interaction
Quando php artisan devto:sync-articles executa novamente
Então nenhuma nova Interaction é criada pro artigo 123
E o engagement_snapshot no metadata da Interaction existente é atualizado
E o coins_awarded NÃO é recalculado (só acontece no momento do review)
Dado que um membro He4rt clica "Conectar DevTo" no perfil
Quando ele completa o fluxo OAuth
Então uma nova ExternalIdentity é criada com provider=devto
E o username e avatar do DevTo são armazenados
E syncs futuros de artigos vão detectar os artigos desse usuárioStep 4: Modificar módulo identity/ — adicionar DevTo ao IdentityProvider
Context
O enum IdentityProvider em app-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.php só tem Discord e Twitch. Precisa adicionar DevTo com as configs correspondentes (icon, color, description, client binding).
Arquivos a modificar:
app-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.php— adicionarcase DevTo = 'devto'config/services.php— adicionar blocodevtocom OAuth credentialsIntegrationDevToServiceProvider— bindDevToOAuthClient
No enum, adicionar métodos pra DevTo:
getClient()→ retornaDevToOAuthClientgetColor()→ preto (#000000)getIcon()→ heroicon adequadogetDescription()→ "Dev.to"getScopes()→ busca deconfig('services.devto.scopes')isEnabled()→ busca deconfig('services.devto.enabled')
Expected Behavior
Dado que o enum IdentityProvider existe com Discord e Twitch
Quando o case DevTo é adicionado
Então IdentityProvider::DevTo->value é igual a 'devto'
E IdentityProvider::DevTo->getClient() retorna instância de DevToOAuthClient
E a URL de redirect OAuth pra DevTo funciona corretamente
E os fluxos existentes de Discord/Twitch não são afetadosStep 5: Admin Filament — InteractionResource (aprovação manual interim)
Context
Enquanto o módulo de peer review não existe, o admin precisa visualizar e aprovar/rejeitar Interactions manualmente via Filament. Criar InteractionResource no painel Admin.
Criar em app-modules/activity/src/Tracking/Filament/Admin/Resources/Interactions/:
InteractionResource.phpPages/ListInteractions.phpPages/ViewInteraction.php(view-only, sem edit genérico)Schemas/InteractionInfolist.php(exibição de detalhes)Tables/InteractionsTable.php
Tabela deve mostrar:
- Status (badge colorido)
- Tipo (ActivityType)
- Provider
- Character (nome do usuário)
- Value tier
- Coins range (min-max)
- Coins awarded (se aprovado)
- Occurred at
- Filtros: status, type, provider, value_tier
Actions na tabela:
- Approve (bulk + individual) — chama ApproveInteraction com CalculateReward
- Reject (bulk + individual) — chama RejectInteraction
Expected Behavior
Dado que existem 10 Interactions com status 'pending'
Quando o admin abre /admin/interactions
Então todas as 10 são listadas com status, tipo, provider e range de coins
E o admin pode filtrar por status='pending'
E o admin pode selecionar múltiplas e aprovar em bulk
Dado que o admin clica "Aprovar" em uma Interaction pendente de artigo
Quando ApproveInteraction é chamado
Então CalculateReward calcula coins_awarded a partir do engagement snapshot
E Economy::Credit é chamado pra wallet do character
E Gamification::IncrementExperience é chamado
E o status da Interaction muda para 'approved'
E reviewed_at é definido como now()Step 6: Conectar com Economy e Gamification
Context
Quando uma Interaction é aprovada (manual ou auto), o sistema precisa creditar coins na wallet do Character e incrementar XP. Usa as Actions existentes do módulo economy/ (Credit) e gamification/ (IncrementExperience).
ApproveInteraction Action:
1. Recebe Interaction
2. Chama CalculateReward → define coins_awarded e xp_awarded
3. Resolve Character → getOrCreateWallet()
4. Economy\Actions\Credit(CreditDTO) com:
- walletId: character.wallet.id
- amount: coins_awarded
- referenceType: Interaction::class
- referenceId: interaction.id
- description: "Reward: {ActivityType->label}"
5. Gamification\Actions\IncrementExperience com xp_awarded
6. Atualiza Interaction: status=approved, reviewed_at=now()
7. Dispara InteractionApproved event
HasInteractions trait (adicionar ao Character):
Segue o padrão verificado em Character::pastSeasons() (@return HasMany<PastSeason, $this>):
trait HasInteractions {
/**
* @return HasMany<Interaction, $this>
*/
public function interactions(): HasMany
{
return $this->hasMany(Interaction::class);
}
}Expected Behavior
Dado um Character com wallet balance 0 e experience 500
E uma Interaction pendente type='article' com coins_min=100, coins_max=300
E engagement_snapshot com reactions=42, bookmarks=8, comments=12
Quando ApproveInteraction é chamado com peer_review_base=200
Então coins_awarded = 253 (200 base + 53 engagement bonus, limitado a 300)
E o balance da wallet passa a ser 253
E existe um registro Transaction com type=Reward, amount=253, reference=Interaction
E a experience do character aumenta em xp_awarded
E o status da Interaction é 'approved'
E o evento InteractionApproved é disparadoStep 7: Testes
Context
Testes unitários para cada Action, testes de feature para o pipeline completo, e testes de integração para o sync do DevTo.
Testes a criar:
app-modules/activity/tests/
Unit/
Tracking/
TrackActivityTest.php
ClassifyActivityTest.php
CalculateRewardTest.php
ApproveInteractionTest.php
RejectInteractionTest.php
Feature/
Tracking/
InteractionPipelineTest.php ← end-to-end: DTO → Interaction → Credit
InteractionDeduplicationTest.php
Filament/Admin/
Interactions/
ListInteractionsTest.php
ApproveInteractionTest.php
app-modules/integration-devto/tests/
Feature/
SyncDevToArticlesTest.php ← mock DevTo API, verify Interactions created
DevToOAuthTest.php ← test OAuth flow
Expected Behavior
Dado que todos os testes existentes passam antes das mudanças
Quando a reestruturação está completa
Então todos os testes existentes de Message/Voice passam com os novos namespaces
E todos os novos testes de Tracking passam
E todos os testes de integração DevTo passam
E php artisan test --compact mostra verdeVerificação end-to-end
composer dump-autoload— sem errosphp artisan test --compact— todos os testes passamvendor/bin/pint --dirty --format agent— código formatadophp artisan migrate— migration da tabela interactions rodaphp artisan devto:sync-articles— executa sem erros (com DevTo API mockada em dev)- Admin panel
/admin/interactions— lista Interactions, approve/reject funcionam - Após approve, verificar wallet balance incrementado e transaction criada
- OAuth DevTo flow — redirect + callback funcionam
Arquivos críticos a modificar/criar
Modificar:
app-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.phpapp-modules/activity/src/Providers/ActivityServiceProvider.phpapp-modules/activity/routes/message-routes.phpapp-modules/activity/database/factories/MessageFactory.phpapp-modules/bot-discord/src/Events/MessageReceivedEvent.phpapp-modules/gamification/src/Character/Models/Character.phpconfig/services.phpbootstrap/providers.php
Criar (novos):
app-modules/activity/src/Tracking/(inteiro sub-domínio)app-modules/activity/database/migrations/xxxx_create_interactions_table.phpapp-modules/activity/config/activity-tracking.phpapp-modules/integration-devto/(inteiro módulo)- Todos os testes listados no Step 7
Mover (reorganizar):
app-modules/activity/src/Models/→src/Message/Models/esrc/Voice/Models/app-modules/activity/src/Actions/→src/Message/Actions/esrc/Voice/Actions/app-modules/activity/src/DTOs/→src/Message/DTOs/esrc/Voice/DTOs/app-modules/activity/src/Http/→src/Message/Http/esrc/Voice/Http/app-modules/activity/src/Filament/→src/Message/Filament/
Reutilizar (já existem):
He4rt\Economy\Actions\Credit— creditar coinsHe4rt\Economy\Actions\CreateWallet— criar walletHe4rt\Economy\DTOs\CreditDTO— DTO do creditHe4rt\Economy\Concerns\HasWallet— trait do CharacterHe4rt\Gamification\Character\Actions\IncrementExperience— incrementar XPHe4rt\Identity\ExternalIdentity\Actions\ResolveExternalIdentity— resolver providerHe4rt\Identity\Auth\DTOs\OAuthStateDTO— state do OAuthOAuthClientContractemapp/Contracts/OAuthClientContract.phpOAuthAccessDTOeOAuthUserDTOemapp-modules/identity/src/Auth/DTOs/