feat: interaction activity tracking module#183
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a new activity tracking subsystem: Interaction Eloquent model, migration, factory, enums (ActivityType, ActivityStatus, ValueTier), DTOs, events, actions (ClassifyActivity, CalculateReward, TrackActivity, ApproveInteraction, RejectInteraction), configuration (activity-tracking.php), trait HasInteractions, message/voice namespace reorganization, Dev.to integration (OAuth client, API client, SyncDevToArticles command), IdentityProvider::DevTo, service provider wiring, and accompanying unit and feature tests. Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Clintonrocha98
left a comment
There was a problem hiding this comment.
PR analisado com atenção e aprovado. LGTM (Looks Good To Me).
As mudanças estão alinhadas com o objetivo da tarefa, apresentam boa clareza na implementação e respeitam as convenções adotadas no projeto. A estrutura do código facilita o entendimento, e não foram encontrados pontos críticos ou inconsistências que impeçam a integração.
A solução demonstra cuidado com organização e manutenibilidade, sem indícios de regressões no comportamento esperado. Código validado e pronto para merge.
app-modules/activity/src/Tracking/Filament/Admin/Resources/Interactions/InteractionResource.php
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app-modules/activity/src/Message/Filament/Admin/Resources/Messages/Tables/MessagesTable.php (1)
39-42:⚠️ Potential issue | 🟡 MinorTypo in label: "Obteined" should be "Obtained".
✏️ Proposed fix
TextColumn::make('obtained_experience') - ->label('Obteined XP') + ->label('Obtained XP') ->numeric() ->sortable(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/src/Message/Filament/Admin/Resources/Messages/Tables/MessagesTable.php` around lines 39 - 42, Typo in the Filament column label: update the TextColumn declaration for obtained_experience (TextColumn::make('obtained_experience')->label(...)) in MessagesTable.php to change the label from "Obteined XP" to "Obtained XP" so the UI shows the correct spelling.app-modules/activity/src/Message/Filament/Admin/Resources/Messages/Schemas/MessageForm.php (1)
33-35:⚠️ Potential issue | 🟡 MinorTypo in label: "Chanel" should be "Channel".
✏️ Proposed fix
TextInput::make('channel_id') - ->label('Chanel') + ->label('Channel') ->nullable(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/src/Message/Filament/Admin/Resources/Messages/Schemas/MessageForm.php` around lines 33 - 35, The TextInput field defined by TextInput::make('channel_id') has a typo in its label (->label('Chanel')), so update the label text to the correct spelling "Channel" by changing the label call on the channel_id TextInput to ->label('Channel').app-modules/activity/src/Message/Filament/Admin/Resources/Messages/MessageResource.php (1)
24-24:⚠️ Potential issue | 🟡 MinorTypo: "Gamefication" should be "Gamification".
The navigation group label contains a spelling error.
📝 Proposed fix
- protected static string|UnitEnum|null $navigationGroup = 'Gamefication'; + protected static string|UnitEnum|null $navigationGroup = 'Gamification';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/src/Message/Filament/Admin/Resources/Messages/MessageResource.php` at line 24, Fix the typo in the MessageResource class by updating the protected static property navigationGroup (protected static string|UnitEnum|null $navigationGroup) value from 'Gamefication' to 'Gamification' so the navigation group label is spelled correctly.
🧹 Nitpick comments (11)
app-modules/activity/tests/Unit/Tracking/RejectInteractionTest.php (1)
12-21: Assert persisted state, not only returned instance.To ensure
RejectInteractionactually saves, reload the model and assert values from DB-backed state.Suggested test hardening
test('rejects interaction', function (): void { $interaction = Interaction::factory()->create([ 'status' => ActivityStatus::Pending, ]); $result = resolve(RejectInteraction::class)->handle($interaction); + $interaction->refresh(); expect($result->status)->toBe(ActivityStatus::Rejected) - ->and($result->reviewed_at)->not->toBeNull(); + ->and($result->reviewed_at)->not->toBeNull() + ->and($interaction->status)->toBe(ActivityStatus::Rejected) + ->and($interaction->reviewed_at)->not->toBeNull(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/tests/Unit/Tracking/RejectInteractionTest.php` around lines 12 - 21, The test currently only asserts the returned instance from RejectInteraction::handle; update it to assert persisted DB state by reloading the Interaction model after calling resolve(RejectInteraction::class)->handle($interaction) (e.g., $interaction->refresh() or Interaction::find($interaction->id)) and then assert that the reloaded model's status is ActivityStatus::Rejected and reviewed_at is not null to ensure the change was saved to the database.app-modules/integration-devto/config/integration-devto.php (1)
8-8: Harden polling interval config against invalid env values.
DEVTO_POLLING_INTERVALshould be normalized to a positive integer to avoid scheduler misconfiguration.Suggested fix
- 'polling_interval_minutes' => env('DEVTO_POLLING_INTERVAL', 30), + 'polling_interval_minutes' => max(1, (int) env('DEVTO_POLLING_INTERVAL', 30)),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/integration-devto/config/integration-devto.php` at line 8, The polling_interval_minutes config uses env('DEVTO_POLLING_INTERVAL', 30) without validation; normalize and enforce a positive integer by reading the env value, casting/parsing it to an integer and falling back to the default when missing/invalid or non-positive (e.g., use intval/ (int) cast and max(1, $value) or is_numeric check), then assign that sanitized value to 'polling_interval_minutes' instead of trusting the raw env call.app-modules/activity/src/Message/Http/Controllers/MessagesController.php (1)
26-34: Consider separating voice functionality into its own controller.The
MessagesControllerunder theMessagenamespace handles both text messages and voice messages. Since voice functionality has been moved to its ownVoicesubdomain (as evidenced by the imports fromHe4rt\Activity\Voice\...), consider movingpostVoiceMessageto a dedicatedVoiceMessagesControllerin the Voice namespace for better separation of concerns.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/src/Message/Http/Controllers/MessagesController.php` around lines 26 - 34, The postVoiceMessage method (CreateVoiceMessageRequest, NewVoiceMessage) currently lives in MessagesController and mixes voice subdomain concerns; move this method into a new VoiceMessagesController inside the Voice namespace (e.g., He4rt\Activity\Voice\Http\Controllers) and update routing to point to VoiceMessagesController::postVoiceMessage, keeping the same signature and behavior (call NewVoiceMessage->persist($request->validated()) and return response()->noContent()); remove the method from MessagesController and adjust imports/usages accordingly.app-modules/activity/tests/Unit/Tracking/ApproveInteractionTest.php (1)
36-41: Consider documenting the expected reward calculation.The assertion
coins_awarded = 253is a magic number. Adding a comment explaining how this value is derived (e.g., frompeerReviewBase: 200, engagement metricsreactions: 42, bookmarks: 8, comments: 12, and coin range100-300) would improve test readability and maintainability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/tests/Unit/Tracking/ApproveInteractionTest.php` around lines 36 - 41, The test uses a magic number 253 for coins_awarded/wallet balance without explanation; update the test near the assertion on coins_awarded (expect($result->coins_awarded)->toBe(253)) and the wallet balance check to include a concise inline comment that documents the reward calculation (e.g., base peerReviewBase = 200 plus engagement contributions from reactions/bookmarks/comments and applied coin range 100-300) so future readers can see how 253 was derived; reference the symbols ActivityStatus::Approved, $result->coins_awarded, and $character->fresh()->wallets()->first() when adding the comment.app-modules/integration-devto/src/OAuth/DevToOAuthUser.php (1)
17-19: Consider adding defensive checks for required payload fields.
payload['id']andpayload['username']are accessed directly without null checks. If the Dev.to API returns an unexpected response structure, this will throw an unclear error. Consider validating required fields or providing clearer error messages.🛡️ Proposed defensive validation
public static function make(OAuthAccessDTO $credentials, array $payload): OAuthUserDTO { + if (!isset($payload['id'], $payload['username'])) { + throw new \InvalidArgumentException('DevTo user payload missing required fields: id, username'); + } + return new self( credentials: $credentials, providerId: (string) $payload['id'],🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/integration-devto/src/OAuth/DevToOAuthUser.php` around lines 17 - 19, The mapping in DevToOAuthUser that assigns providerId and username directly from $payload risks undefined index errors; update the code in DevToOAuthUser to defensively validate that $payload['id'] and $payload['username'] exist and are non-empty before using them (e.g., check isset/empty or use null-coalescing), and if missing throw or return a clear, descriptive exception/error (include which field is missing and reference IdentityProvider::DevTo in the message) so callers can handle malformed Dev.to responses safely.app-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.php (1)
47-49: Potential N+1 query issue withmessages_countin$appends.The
messages_countaccessor executes$this->messages()->count()on every model serialization. When loading multipleExternalIdentityrecords, this triggers a separate COUNT query per record.Consider using
withCount('messages')when querying, and conditionally appending the attribute only when the count has been loaded:♻️ Proposed refactor to avoid N+1 queries
protected $appends = [ - 'messages_count', ];Then load the count explicitly where needed:
ExternalIdentity::withCount('messages')->get();Or make the accessor conditional:
protected function getMessagesCountAttribute(): int { + if ($this->relationLoaded('messages')) { + return $this->messages->count(); + } + if (array_key_exists('messages_count', $this->attributes)) { + return $this->attributes['messages_count']; + } return $this->messages()->count(); }Also applies to: 92-95
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.php` around lines 47 - 49, The messages_count accessor added via $appends causes an N+1 because getMessagesCountAttribute calls $this->messages()->count() for every model; change the accessor (getMessagesCountAttribute) to first return $this->attributes['messages_count'] if present (this is set by ExternalIdentity::withCount('messages')), then fall back to $this->relationLoaded('messages') ? $this->messages->count() : null (or only then call $this->messages()->count() if you really need a DB hit), and remove unconditional appends usage (or only append 'messages_count' when you explicitly loaded it) so callers should load counts with withCount('messages') when retrieving multiple ExternalIdentity records.app-modules/activity/config/activity-tracking.php (1)
6-22: Consider using enum values in config keys to reduce drift risk.Using raw strings for activity/tier names can silently diverge from enums over time. Prefer enum-backed keys/values where possible.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/config/activity-tracking.php` around lines 6 - 22, Replace raw string activity and tier names in the 'classification' array and the 'auto_approve_tiers' list with the corresponding enum values to prevent drift; specifically, use your ActivityType enum members (e.g., ActivityType::ARTICLE, ActivityType::PR_MERGED, etc.) as the keys for the 'classification' map and use ActivityTier enum members (e.g., ActivityTier::HIGH, ActivityTier::MEDIUM, ActivityTier::LOW) for the 'tier' fields and entries in 'auto_approve_tiers' (use ->value or ::value depending on your PHP enum implementation) so the config references the enums ActivityType and ActivityTier instead of raw strings.app-modules/integration-devto/composer.json (1)
8-13: Move test and database namespaces toautoload-dev.
He4rt\\IntegrationDevTo\\Tests\\,He4rt\\IntegrationDevTo\\Database\\Factories\\, andHe4rt\\IntegrationDevTo\\Database\\Seeders\\are currently inautoloadbut should be inautoload-dev. This keeps the production autoloader lean and prevents these namespaces from being loaded when the package is used as a dependency in non-dev environments.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/integration-devto/composer.json` around lines 8 - 13, The composer.json currently lists test and database namespaces under "autoload" which should be moved to "autoload-dev" to avoid shipping dev-only classes in production; relocate the PSR-4 entries "He4rt\\IntegrationDevTo\\Tests\\", "He4rt\\IntegrationDevTo\\Database\\Factories\\", and "He4rt\\IntegrationDevTo\\Database\\Seeders\\" from the "autoload" -> "psr-4" block into a new or existing "autoload-dev" -> "psr-4" block, ensuring JSON syntax remains valid (commas, braces) after the change.app-modules/activity/src/Tracking/Actions/TrackActivity.php (1)
36-50: Consider adding the interaction creation to the same transaction.If the goal is to ensure atomicity, the
Interaction::query()->create()call should also be within the transaction scope. Currently, if the auto-approval logic fails after creation, a pending-like interaction would remain.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/src/Tracking/Actions/TrackActivity.php` around lines 36 - 50, The interaction is created outside the transaction so failures later (e.g., auto-approval) can leave a partial state; move the Interaction::query()->create(...) into the same DB transaction scope (or pass the active transaction/connection to the create call) used for the subsequent logic in TrackActivity.php so creation and auto-approval are atomic; locate the transaction block and replace the external Interaction::query()->create with a create executed inside that transaction (or use the transaction's query builder/connection when calling Interaction::query()).app-modules/integration-devto/src/Polling/SyncDevToArticles.php (2)
93-103: Consider wrapping API call in try-catch to prevent single article failure from stopping sync.If
getArticle()fails for one article, the entire sync command will crash. Catching the exception and logging would allow processing to continue for other articles.♻️ Proposed improvement
if ($existingInteraction !== null) { - $articleDetails = $this->apiClient->getArticle($article['id']); + try { + $articleDetails = $this->apiClient->getArticle($article['id']); + } catch (\Throwable $e) { + Log::warning('DevTo sync: failed to fetch article details for update', [ + 'article_id' => $article['id'], + 'error' => $e->getMessage(), + ]); + return 'skipped'; + } $existingInteraction->update([Apply similar error handling to line 119.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/integration-devto/src/Polling/SyncDevToArticles.php` around lines 93 - 103, In SyncDevToArticles, wrap the call to $this->apiClient->getArticle($article['id']) and the subsequent $existingInteraction->update(...) in a try-catch that catches exceptions from getArticle() (and from update), logs the error with context (article id and exception message) and continues to the next article so a single failure doesn't stop the whole sync; apply the same try-catch pattern to the other API call around line 119 as well so both getArticle-related failures are handled gracefully.
41-55: Extract page size as a constant and add resilience for API failures.The magic number
30should be a named constant for clarity. Additionally, the pagination loop lacks error handling - ifgetArticlesByOrg()throws, the command fails without partial progress being saved.♻️ Proposed improvement
+ private const PAGE_SIZE = 30; + public function handle(): int { $orgSlug = config('integration-devto.org_slug'); $page = 1; $totalCreated = 0; $totalUpdated = 0; $totalSkipped = 0; $this->info('Syncing articles from DevTo org: '.$orgSlug); do { - $articles = $this->apiClient->getArticlesByOrg($orgSlug, $page); + try { + $articles = $this->apiClient->getArticlesByOrg($orgSlug, $page); + } catch (\Throwable $e) { + Log::error('DevTo sync: failed to fetch articles', [ + 'page' => $page, + 'error' => $e->getMessage(), + ]); + $this->error('Failed to fetch articles from DevTo API: '.$e->getMessage()); + break; + } foreach ($articles as $article) { $result = $this->processArticle($article); match ($result) { 'created' => $totalCreated++, 'updated' => $totalUpdated++, 'skipped' => $totalSkipped++, }; } $page++; - } while (count($articles) === 30); + } while (count($articles) === self::PAGE_SIZE);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/integration-devto/src/Polling/SyncDevToArticles.php` around lines 41 - 55, Extract the magic number 30 into a named constant (e.g., private const PAGE_SIZE = 30) and use that constant in the pagination condition instead of the literal; wrap the API call to $this->apiClient->getArticlesByOrg($orgSlug, $page) in a try/catch so failures don't abort the whole run—on exception, log the error via the class logger, stop the loop (or set $articles = [] and break) so partial progress (the $totalCreated/$totalUpdated/$totalSkipped counts from processArticle) is preserved, and only increment $page after a successful fetch; keep references to SyncDevToArticles, getArticlesByOrg, and processArticle to locate the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@app-modules/activity/database/migrations/2026_03_18_000000_create_interactions_table.php`:
- Line 26: The migration currently makes external_ref globally unique which can
cause cross-tenant/provider collisions; update the migration in
create_interactions_table (remove ->unique() from
$table->string('external_ref')->unique()->nullable()) and instead add a
composite unique index such as
$table->unique(['tenant_id','provider','external_ref']) (or whatever
tenant/provider column names are used) so uniqueness is enforced per tenant and
provider.
In `@app-modules/activity/src/Tracking/Actions/ApproveInteraction.php`:
- Around line 20-44: Wrap the entire approval flow in a DB transaction and make
it single-use by reloading and locking the Interaction row for update, verifying
its status is ActivityStatus::Pending before proceeding; then run
calculateReward->handle, credit the wallet (Credit::class with CreditDTO),
increment character experience, and set status/reviewed_at with
interaction->update inside the transaction, and dispatch InteractionApproved
only after the transaction commits (use DB::transaction with DB::afterCommit or
equivalent) and return the fresh interaction; if the status is not Pending,
abort/throw to prevent double-approve.
In `@app-modules/activity/src/Tracking/Actions/CalculateReward.php`:
- Around line 22-45: The final computed reward ($coinsAwarded) is only clamped
to the upper bound; ensure it is clamped into the full allowed range [coins_min,
coins_max] before it's used (including the branch where $engagementSnapshot is
null and when $peerReviewBase is used). Update the logic in CalculateReward
(affecting $coinsAwarded computation) to apply min(max($value,
$interaction->coins_min), $interaction->coins_max) (or equivalent) for both the
engagementSnapshot branch and the else branch so negative or too-small
peerReviewBase values cannot fall below $interaction->coins_min before being
passed to Credit.
In `@app-modules/activity/src/Tracking/Actions/RejectInteraction.php`:
- Around line 12-19: Restrict the state change in handle(Interaction
$interaction) to only update rows currently in ActivityStatus::Pending and fail
if none were affected: perform a conditional update via Interaction::where('id',
$interaction->id)->where('status', ActivityStatus::Pending)->update([ 'status'
=> ActivityStatus::Rejected, 'reviewed_at' => now() ]) and check the returned
affected-rows count; if zero, throw an exception (or return a clear failure) to
signal an invalid transition/race, otherwise return the fresh interaction
record.
In `@app-modules/activity/src/Tracking/Actions/TrackActivity.php`:
- Around line 52-67: Wrap the auto-approval block that handles reward
calculation, wallet credit and XP increment in a database transaction to ensure
atomicity: when $classification['status'] === ActivityStatus::AutoApproved,
perform $this->calculateReward->handle($interaction), find the Character
(Character::query()->findOrFail($dto->characterId)), call
resolve(Credit::class)->handle(new CreditDTO(...)) and
$character->increment('experience', ...) inside a DB::transaction (or equivalent
transactional helper) so failures roll back; consider extending the transaction
to include interaction creation by wrapping the entire handle() method if full
atomicity is desired.
In
`@app-modules/activity/src/Tracking/Filament/Admin/Resources/Interactions/InteractionResource.php`:
- Line 21: The navigation group string on InteractionResource (protected static
string|UnitEnum|null $navigationGroup) is misspelled as 'Gamefication'; update
that value to the correct spelling 'Gamification' so the admin navigation
displays the proper group name.
- Line 25: The resource currently sets protected static ?string
$recordTitleAttribute = 'type' but 'type' is cast to the backed string enum
ActivityType, which can cause unreliable rendering; fix by either adding a
string accessor on the model (e.g. getTypeLabelAttribute or
getTypeDisplayAttribute that returns (string) $this->type or uses
ActivityType::from(...)->label) and point recordTitleAttribute at that accessor
(e.g. 'type_label'), or override the resource title formatter in
InteractionResource (implement a getTitle() / recordTitleUsing() style hook to
return a string from $record->type->value or $record->type->label); reference
InteractionResource::$recordTitleAttribute and the model's type attribute /
ActivityType enum when making the change.
In `@app-modules/activity/src/Voice/Http/Requests/CreateVoiceMessageRequest.php`:
- Around line 19-21: The provider validation in CreateVoiceMessageRequest.php
incorrectly includes "devto" for voice messages; update the 'provider' rule (in
the validation array inside CreateVoiceMessageRequest) to remove "devto" so it
only allows the actual voice providers (e.g., 'twitch,discord'), and keep the
existing 'provider_id' and 'state' rules unchanged; if devto was intentionally
added, instead adjust the allowed 'state' values or add conditional validation
logic in CreateVoiceMessageRequest to handle non-voice providers appropriately.
In `@app-modules/activity/tests/Unit/Actions/NewMessageTest.php`:
- Around line 5-7: The test constructs NewMessage and calls persist using the
old signature and is skipped; update the test to instantiate NewMessage with its
current constructor signature, pass a NewMessageDTO instance (not an array) to
NewMessage->persist(), and update the PersistMessage mock to expect and return
values for a NewMessageDTO parameter; remove or disable the skip so the
assertions run. Specifically, replace the old NewMessage(...) construction with
the new constructor usage, create a NewMessageDTO (using the same test payload
fields), have the PersistMessage mock expect persist(NewMessageDTO $dto) and
return the expected result, then call $newMessage->persist($dto) and assert
outcomes.
In `@app-modules/activity/tests/Unit/Tracking/CalculateRewardTest.php`:
- Around line 43-48: The test titled "uses coins_min when no engagement and auto
approved" doesn't mark the Interaction as auto-approved, so update the
Interaction fixture created via Interaction::factory() to explicitly set the
status to the auto-approved value (e.g., 'approved' or the relevant constant) by
adding a 'status' => 'approved' (or Interaction::STATUS_APPROVED) entry to the
factory payload for this test; alternatively, if you intend to test the pending
case, rename the test to reflect that behavior so the test name matches the
fixture.
In `@app-modules/gamification/src/Character/Models/Character.php`:
- Line 37: Character::wallet() currently defines a HasOne Eloquent relation and
thereby overrides HasWallet::wallet(Currency $currency = Currency::Coin):
?Wallet causing the trait's currency-filtering API to be lost; fix by renaming
the relation method (e.g., to walletRelation() or walletsRelation()) and
keeping/adding a compatibility method matching HasWallet::wallet(Currency
$currency = Currency::Coin): ?Wallet that delegates to the relation (e.g., call
$this->wallets()->where('currency', $currency)->first()) or explicitly calls the
trait implementation via HasWallet::wallet($currency); update any internal call
sites to use the new relation name.
In `@app-modules/integration-devto/src/OAuth/DevToOAuthAccessDTO.php`:
- Around line 11-16: Validate the incoming OAuth payload in
DevToOAuthAccessDTO::make before constructing the DTO: ensure 'access_token'
exists and is a non-empty string (throw an InvalidArgumentException with a clear
message if missing), coerce/validate 'expires_in' to an int or null (reject
non-numeric values), and ensure 'refresh_token' is a string (use empty string
default only after validation); then pass the validated/typed values into the
DevToOAuthAccessDTO constructor so malformed provider responses produce a
controlled exception rather than causing downstream errors.
In `@app-modules/integration-devto/src/OAuth/DevToOAuthClient.php`:
- Around line 39-45: The getAuthenticatedUser method currently calls the Dev.to
API without checking for HTTP errors; update getAuthenticatedUser(OAuthAccessDTO
$credentials) to verify the $response status (e.g., using
$response->successful() or checking $response->failed()) before passing data to
DevToOAuthUser::make, and when the call fails throw or return a meaningful
exception including status code and response body (or log the details) so
callers receive clear error information instead of malformed user data.
- Around line 26-37: The auth method in DevToOAuthClient.php currently posts to
Dev.to and immediately calls ->json() without handling failures; update
auth(string $code) to handle HTTP/network errors by either using the HTTP
client's ->throw() before ->json() or by checking $response->successful() /
$response->ok() and throwing a meaningful exception when the request failed, and
only then pass the validated response data into DevToOAuthAccessDTO::make;
ensure you catch network exceptions (e.g., RequestException) if using try/catch
and include the error context in the thrown exception or log.
In `@app-modules/integration-devto/src/Polling/DevToApiClient.php`:
- Around line 15-31: The Dev.to API calls in getArticles and getArticle silently
return an empty array on failures; update both methods to use Laravel's Http
facade with timeout, retry, and throw so failures surface and are
observable—call Http::timeout(…)->retry(…, …)->throw()->get(...) (or equivalent)
when building the request, and remove the silent fallback that masks errors so
the methods propagate exceptions instead of returning [] on non-2xx responses;
locate these changes in the getArticles (or similar list-fetching method) and
getArticle functions shown in DevToApiClient.
---
Outside diff comments:
In
`@app-modules/activity/src/Message/Filament/Admin/Resources/Messages/MessageResource.php`:
- Line 24: Fix the typo in the MessageResource class by updating the protected
static property navigationGroup (protected static string|UnitEnum|null
$navigationGroup) value from 'Gamefication' to 'Gamification' so the navigation
group label is spelled correctly.
In
`@app-modules/activity/src/Message/Filament/Admin/Resources/Messages/Schemas/MessageForm.php`:
- Around line 33-35: The TextInput field defined by
TextInput::make('channel_id') has a typo in its label (->label('Chanel')), so
update the label text to the correct spelling "Channel" by changing the label
call on the channel_id TextInput to ->label('Channel').
In
`@app-modules/activity/src/Message/Filament/Admin/Resources/Messages/Tables/MessagesTable.php`:
- Around line 39-42: Typo in the Filament column label: update the TextColumn
declaration for obtained_experience
(TextColumn::make('obtained_experience')->label(...)) in MessagesTable.php to
change the label from "Obteined XP" to "Obtained XP" so the UI shows the correct
spelling.
---
Nitpick comments:
In `@app-modules/activity/config/activity-tracking.php`:
- Around line 6-22: Replace raw string activity and tier names in the
'classification' array and the 'auto_approve_tiers' list with the corresponding
enum values to prevent drift; specifically, use your ActivityType enum members
(e.g., ActivityType::ARTICLE, ActivityType::PR_MERGED, etc.) as the keys for the
'classification' map and use ActivityTier enum members (e.g.,
ActivityTier::HIGH, ActivityTier::MEDIUM, ActivityTier::LOW) for the 'tier'
fields and entries in 'auto_approve_tiers' (use ->value or ::value depending on
your PHP enum implementation) so the config references the enums ActivityType
and ActivityTier instead of raw strings.
In `@app-modules/activity/src/Message/Http/Controllers/MessagesController.php`:
- Around line 26-34: The postVoiceMessage method (CreateVoiceMessageRequest,
NewVoiceMessage) currently lives in MessagesController and mixes voice subdomain
concerns; move this method into a new VoiceMessagesController inside the Voice
namespace (e.g., He4rt\Activity\Voice\Http\Controllers) and update routing to
point to VoiceMessagesController::postVoiceMessage, keeping the same signature
and behavior (call NewVoiceMessage->persist($request->validated()) and return
response()->noContent()); remove the method from MessagesController and adjust
imports/usages accordingly.
In `@app-modules/activity/src/Tracking/Actions/TrackActivity.php`:
- Around line 36-50: The interaction is created outside the transaction so
failures later (e.g., auto-approval) can leave a partial state; move the
Interaction::query()->create(...) into the same DB transaction scope (or pass
the active transaction/connection to the create call) used for the subsequent
logic in TrackActivity.php so creation and auto-approval are atomic; locate the
transaction block and replace the external Interaction::query()->create with a
create executed inside that transaction (or use the transaction's query
builder/connection when calling Interaction::query()).
In `@app-modules/activity/tests/Unit/Tracking/ApproveInteractionTest.php`:
- Around line 36-41: The test uses a magic number 253 for coins_awarded/wallet
balance without explanation; update the test near the assertion on coins_awarded
(expect($result->coins_awarded)->toBe(253)) and the wallet balance check to
include a concise inline comment that documents the reward calculation (e.g.,
base peerReviewBase = 200 plus engagement contributions from
reactions/bookmarks/comments and applied coin range 100-300) so future readers
can see how 253 was derived; reference the symbols ActivityStatus::Approved,
$result->coins_awarded, and $character->fresh()->wallets()->first() when adding
the comment.
In `@app-modules/activity/tests/Unit/Tracking/RejectInteractionTest.php`:
- Around line 12-21: The test currently only asserts the returned instance from
RejectInteraction::handle; update it to assert persisted DB state by reloading
the Interaction model after calling
resolve(RejectInteraction::class)->handle($interaction) (e.g.,
$interaction->refresh() or Interaction::find($interaction->id)) and then assert
that the reloaded model's status is ActivityStatus::Rejected and reviewed_at is
not null to ensure the change was saved to the database.
In `@app-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.php`:
- Around line 47-49: The messages_count accessor added via $appends causes an
N+1 because getMessagesCountAttribute calls $this->messages()->count() for every
model; change the accessor (getMessagesCountAttribute) to first return
$this->attributes['messages_count'] if present (this is set by
ExternalIdentity::withCount('messages')), then fall back to
$this->relationLoaded('messages') ? $this->messages->count() : null (or only
then call $this->messages()->count() if you really need a DB hit), and remove
unconditional appends usage (or only append 'messages_count' when you explicitly
loaded it) so callers should load counts with withCount('messages') when
retrieving multiple ExternalIdentity records.
In `@app-modules/integration-devto/composer.json`:
- Around line 8-13: The composer.json currently lists test and database
namespaces under "autoload" which should be moved to "autoload-dev" to avoid
shipping dev-only classes in production; relocate the PSR-4 entries
"He4rt\\IntegrationDevTo\\Tests\\",
"He4rt\\IntegrationDevTo\\Database\\Factories\\", and
"He4rt\\IntegrationDevTo\\Database\\Seeders\\" from the "autoload" -> "psr-4"
block into a new or existing "autoload-dev" -> "psr-4" block, ensuring JSON
syntax remains valid (commas, braces) after the change.
In `@app-modules/integration-devto/config/integration-devto.php`:
- Line 8: The polling_interval_minutes config uses env('DEVTO_POLLING_INTERVAL',
30) without validation; normalize and enforce a positive integer by reading the
env value, casting/parsing it to an integer and falling back to the default when
missing/invalid or non-positive (e.g., use intval/ (int) cast and max(1, $value)
or is_numeric check), then assign that sanitized value to
'polling_interval_minutes' instead of trusting the raw env call.
In `@app-modules/integration-devto/src/OAuth/DevToOAuthUser.php`:
- Around line 17-19: The mapping in DevToOAuthUser that assigns providerId and
username directly from $payload risks undefined index errors; update the code in
DevToOAuthUser to defensively validate that $payload['id'] and
$payload['username'] exist and are non-empty before using them (e.g., check
isset/empty or use null-coalescing), and if missing throw or return a clear,
descriptive exception/error (include which field is missing and reference
IdentityProvider::DevTo in the message) so callers can handle malformed Dev.to
responses safely.
In `@app-modules/integration-devto/src/Polling/SyncDevToArticles.php`:
- Around line 93-103: In SyncDevToArticles, wrap the call to
$this->apiClient->getArticle($article['id']) and the subsequent
$existingInteraction->update(...) in a try-catch that catches exceptions from
getArticle() (and from update), logs the error with context (article id and
exception message) and continues to the next article so a single failure doesn't
stop the whole sync; apply the same try-catch pattern to the other API call
around line 119 as well so both getArticle-related failures are handled
gracefully.
- Around line 41-55: Extract the magic number 30 into a named constant (e.g.,
private const PAGE_SIZE = 30) and use that constant in the pagination condition
instead of the literal; wrap the API call to
$this->apiClient->getArticlesByOrg($orgSlug, $page) in a try/catch so failures
don't abort the whole run—on exception, log the error via the class logger, stop
the loop (or set $articles = [] and break) so partial progress (the
$totalCreated/$totalUpdated/$totalSkipped counts from processArticle) is
preserved, and only increment $page after a successful fetch; keep references to
SyncDevToArticles, getArticlesByOrg, and processArticle to locate the changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5c21423f-48e8-48a9-bd8c-a0262f536ea1
⛔ Files ignored due to path filters (1)
composer.lockis excluded by!**/*.lock
📒 Files selected for processing (71)
app-modules/activity/config/activity-tracking.phpapp-modules/activity/database/factories/InteractionFactory.phpapp-modules/activity/database/factories/MessageFactory.phpapp-modules/activity/database/migrations/2026_03_18_000000_create_interactions_table.phpapp-modules/activity/routes/message-routes.phpapp-modules/activity/src/Message/Actions/NewMessage.phpapp-modules/activity/src/Message/Actions/PersistMessage.phpapp-modules/activity/src/Message/DTOs/NewMessageDTO.phpapp-modules/activity/src/Message/Filament/Admin/Resources/Messages/MessageResource.phpapp-modules/activity/src/Message/Filament/Admin/Resources/Messages/Pages/CreateMessage.phpapp-modules/activity/src/Message/Filament/Admin/Resources/Messages/Pages/EditMessage.phpapp-modules/activity/src/Message/Filament/Admin/Resources/Messages/Pages/ListMessages.phpapp-modules/activity/src/Message/Filament/Admin/Resources/Messages/Schemas/MessageForm.phpapp-modules/activity/src/Message/Filament/Admin/Resources/Messages/Tables/MessagesTable.phpapp-modules/activity/src/Message/Http/Controllers/MessagesController.phpapp-modules/activity/src/Message/Http/Requests/CreateMessageRequest.phpapp-modules/activity/src/Message/Models/Message.phpapp-modules/activity/src/Providers/ActivityServiceProvider.phpapp-modules/activity/src/Tracking/Actions/ApproveInteraction.phpapp-modules/activity/src/Tracking/Actions/CalculateReward.phpapp-modules/activity/src/Tracking/Actions/ClassifyActivity.phpapp-modules/activity/src/Tracking/Actions/RejectInteraction.phpapp-modules/activity/src/Tracking/Actions/TrackActivity.phpapp-modules/activity/src/Tracking/Concerns/HasInteractions.phpapp-modules/activity/src/Tracking/Contracts/ActivitySourceContract.phpapp-modules/activity/src/Tracking/DTOs/TrackActivityDTO.phpapp-modules/activity/src/Tracking/Enums/ActivityStatus.phpapp-modules/activity/src/Tracking/Enums/ActivityType.phpapp-modules/activity/src/Tracking/Enums/ValueTier.phpapp-modules/activity/src/Tracking/Events/InteractionApproved.phpapp-modules/activity/src/Tracking/Events/InteractionTracked.phpapp-modules/activity/src/Tracking/Filament/Admin/Resources/Interactions/InteractionResource.phpapp-modules/activity/src/Tracking/Filament/Admin/Resources/Interactions/Pages/ListInteractions.phpapp-modules/activity/src/Tracking/Filament/Admin/Resources/Interactions/Pages/ViewInteraction.phpapp-modules/activity/src/Tracking/Filament/Admin/Resources/Interactions/Tables/InteractionsTable.phpapp-modules/activity/src/Tracking/Models/Interaction.phpapp-modules/activity/src/Voice/Actions/NewVoiceMessage.phpapp-modules/activity/src/Voice/DTOs/NewVoiceMessageDTO.phpapp-modules/activity/src/Voice/Http/Requests/CreateVoiceMessageRequest.phpapp-modules/activity/src/Voice/Models/Voice.phpapp-modules/activity/tests/Feature/Filament/Admin/Message/CreateMessageTest.phpapp-modules/activity/tests/Feature/Filament/Admin/Message/DeleteMessageTest.phpapp-modules/activity/tests/Feature/Filament/Admin/Message/EditMessageTest.phpapp-modules/activity/tests/Feature/Filament/Admin/Message/ListMessageTest.phpTest.phpapp-modules/activity/tests/Feature/Filament/Admin/Message/MessageResourceTest.phpapp-modules/activity/tests/Unit/Actions/NewMessageTest.phpapp-modules/activity/tests/Unit/Tracking/ApproveInteractionTest.phpapp-modules/activity/tests/Unit/Tracking/CalculateRewardTest.phpapp-modules/activity/tests/Unit/Tracking/ClassifyActivityTest.phpapp-modules/activity/tests/Unit/Tracking/RejectInteractionTest.phpapp-modules/activity/tests/Unit/Tracking/TrackActivityTest.phpapp-modules/bot-discord/src/Events/MessageReceivedEvent.phpapp-modules/gamification/src/Character/Models/Character.phpapp-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.phpapp-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.phpapp-modules/identity/src/Tenant/Models/Tenant.phpapp-modules/identity/tests/Feature/FindProfileTest.phpapp-modules/identity/tests/Feature/UpdateProfileTest.phpapp-modules/integration-devto/composer.jsonapp-modules/integration-devto/config/integration-devto.phpapp-modules/integration-devto/src/OAuth/DevToOAuthAccessDTO.phpapp-modules/integration-devto/src/OAuth/DevToOAuthClient.phpapp-modules/integration-devto/src/OAuth/DevToOAuthUser.phpapp-modules/integration-devto/src/Polling/DevToApiClient.phpapp-modules/integration-devto/src/Polling/SyncDevToArticles.phpapp-modules/integration-devto/src/Providers/IntegrationDevToServiceProvider.phpapp-modules/integration-devto/tests/Feature/DevToOAuthTest.phpapp-modules/integration-devto/tests/Feature/SyncDevToArticlesTest.phpcomposer.jsonconfig/services.phpdatabase/seeders/BaseSeeder.php
app-modules/activity/database/migrations/2026_03_18_000000_create_interactions_table.php
Outdated
Show resolved
Hide resolved
app-modules/activity/src/Tracking/Actions/ApproveInteraction.php
Outdated
Show resolved
Hide resolved
- Move Message model, actions, DTOs, controllers, requests to Message/ subdomain - Move Voice model, actions, DTOs, requests to Voice/ subdomain - Update namespaces in ActivityServiceProvider - Update routes and MessageReceivedEvent imports - Update related tests
- Add Interaction model with polymorphic source, value tier, and status - Add ActivityType, ActivityStatus, ValueTier enums - Add TrackActivity, ClassifyActivity, CalculateReward actions - Add ApproveInteraction, RejectInteraction actions for admin review - Add ActivitySourceContract for provider implementations - Add HasInteractions trait for Character model - Add InteractionTracked, InteractionApproved events - Add config/activity-tracking.php with classification rules - Add migration for interactions table - Add unit tests for all Tracking actions
- Add DevToOAuthClient implementing OAuthClientContract - Add DevToOAuthAccessDTO and DevToOAuthUser - Add DevToApiClient for DevTo API wrapper - Add SyncDevToArticles artisan command for scheduled polling - Add IntegrationDevToServiceProvider - Add config/integration-devto.php with org_slug and polling settings - Add feature tests for OAuth and sync
- Add DevTo case to IdentityProvider enum with OAuth client binding - Add getClient(), getColor(), getIcon(), getDescription(), getScopes(), isEnabled() methods - Add devto config block to services.php - Add ExternalIdentity DevTo relationship support - Update tests for FindProfile and UpdateProfile
- Add HasInteractions trait for interactions relationship - Use trait in Character model for activity tracking integration
Move InteractionResource to panel-admin module, fix activity subdomain imports across panel-admin, adapt DevTo integration to use metadata->username instead of direct column, and load activity-tracking config.
c428109
bfa2053 to
c428109
Compare
- Make ApproveInteraction atomic with DB transaction and pessimistic lock to prevent double-crediting on concurrent approvals - Guard both approve and reject actions to only operate on pending interactions - Scope external_ref unique constraint to tenant+provider to avoid cross-tenant collisions - Remove devto from voice message provider validation (no voice support) - Update ValueTier colors: red for High, yellow for Medium (reviewer feedback)
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
app-modules/activity/src/Tracking/Actions/CalculateReward.php (1)
22-45:⚠️ Potential issue | 🟠 MajorClamp rewards to the full configured range.
A low or negative
peerReviewBasecan still pushcoins_awardedbelowcoins_minin both branches here. Clamp the final value into[coins_min, coins_max]before persisting it.Suggested fix
$metadata = $interaction->metadata ?? []; $engagementSnapshot = $metadata['engagement_snapshot'] ?? null; + $clampReward = fn (int $value): int => max( + $interaction->coins_min, + min($value, $interaction->coins_max), + ); if ($engagementSnapshot !== null) { $base = $peerReviewBase ?? (int) (($interaction->coins_min + $interaction->coins_max) / 2); @@ - $coinsAwarded = min($base + $engagementBonus, $interaction->coins_max); + $coinsAwarded = $clampReward($base + $engagementBonus); } else { $coinsAwarded = $peerReviewBase !== null - ? min($peerReviewBase, $interaction->coins_max) + ? $clampReward($peerReviewBase) : $interaction->coins_min; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/src/Tracking/Actions/CalculateReward.php` around lines 22 - 45, In CalculateReward, the final coinsAwarded can still fall outside the configured range when peerReviewBase is low/negative; after computing coinsAwarded in both the engagementSnapshot branch and the else branch, clamp it to the interaction bounds by ensuring coinsAwarded = max(interaction->coins_min, min(coinsAwarded, interaction->coins_max)) (or equivalent) before persisting or returning it so the value always lies within [coins_min, coins_max]; update references where coinsAwarded is used/saved to use the clamped value.app-modules/activity/src/Tracking/Actions/TrackActivity.php (1)
36-69:⚠️ Potential issue | 🟠 MajorMake interaction creation and auto-approval atomic.
If any step after the insert fails, the interaction is already persisted and
CalculateRewardmay already have saved reward fields before the wallet credit or XP increment completes. Wrap creation plus the auto-approved side effects in one transaction, then dispatchInteractionTrackedafter commit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/activity/src/Tracking/Actions/TrackActivity.php` around lines 36 - 69, Wrap the Interaction creation plus its auto-approval side effects in a single DB transaction so either all of creation, reward calculation/crediting, and XP increment succeed or none do, and only dispatch InteractionTracked after the transaction commits; specifically, move the Interaction::query()->create call and the block that checks ActivityStatus::AutoApproved (including $this->calculateReward->handle($interaction), resolving Credit::class->handle(...), and $character->increment(...)) into a DB::transaction (or equivalent) and use a post-commit callback (e.g., afterCommit or dispatching the event after the transaction returns) to call event(new InteractionTracked($interaction)). Ensure you still load Character via Character::query()->findOrFail and getOrCreateWallet() inside the transaction so the wallet credit happens atomically with the interaction creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app-modules/activity/src/Tracking/Actions/TrackActivity.php`:
- Around line 24-30: The dedupe check in TrackActivity that uses
Interaction::query()->where(...)->exists() before create is racy; replace the
two-step exists()+create() with a single atomic operation (e.g.
Interaction::firstOrCreate(['external_ref' => $dto->externalRef], $attributes)
or use Interaction::insertOrIgnore([...]) and then check the insertion result)
and/or catch the unique constraint QueryException (SQLSTATE 23000) around
Interaction::create(...) to treat the duplicate-key error as “already tracked”
and return null; apply the same change to the second duplicate-check block
(lines ~36-50) so both dedupe paths are atomic and race-safe.
In `@app-modules/activity/src/Tracking/DTOs/TrackActivityDTO.php`:
- Around line 13-23: Add a PHPDoc `@param` annotation to the TrackActivityDTO
constructor to document the metadata shape: update the docblock for the
__construct method in class TrackActivityDTO to include "@param
array<string,mixed>|null $metadata" (matching the existing ?array $metadata
parameter) so static analysis (PhpStan) recognizes the key/value types used
across payloads like engagement_snapshot, reactions, comments, bookmarks, etc.
In `@app-modules/activity/src/Tracking/Models/Interaction.php`:
- Around line 21-43: Add the missing docblock annotation and tighten the
metadata type: update the Interaction model's class-level PHPDoc to include the
use annotation /** `@use` HasFactory<InteractionFactory> */ immediately above the
use HasFactory; line and change the `@property` for $metadata from array|null to
array<string, mixed>|null so it matches project typing; keep all other docblock
properties intact and ensure the annotation references the InteractionFactory
type and the class name Interaction.
---
Duplicate comments:
In `@app-modules/activity/src/Tracking/Actions/CalculateReward.php`:
- Around line 22-45: In CalculateReward, the final coinsAwarded can still fall
outside the configured range when peerReviewBase is low/negative; after
computing coinsAwarded in both the engagementSnapshot branch and the else
branch, clamp it to the interaction bounds by ensuring coinsAwarded =
max(interaction->coins_min, min(coinsAwarded, interaction->coins_max)) (or
equivalent) before persisting or returning it so the value always lies within
[coins_min, coins_max]; update references where coinsAwarded is used/saved to
use the clamped value.
In `@app-modules/activity/src/Tracking/Actions/TrackActivity.php`:
- Around line 36-69: Wrap the Interaction creation plus its auto-approval side
effects in a single DB transaction so either all of creation, reward
calculation/crediting, and XP increment succeed or none do, and only dispatch
InteractionTracked after the transaction commits; specifically, move the
Interaction::query()->create call and the block that checks
ActivityStatus::AutoApproved (including
$this->calculateReward->handle($interaction), resolving
Credit::class->handle(...), and $character->increment(...)) into a
DB::transaction (or equivalent) and use a post-commit callback (e.g.,
afterCommit or dispatching the event after the transaction returns) to call
event(new InteractionTracked($interaction)). Ensure you still load Character via
Character::query()->findOrFail and getOrCreateWallet() inside the transaction so
the wallet credit happens atomically with the interaction creation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cc830aa9-199a-46bf-8d84-2c9f5935e02e
📒 Files selected for processing (53)
app-modules/activity/config/activity-tracking.phpapp-modules/activity/database/factories/InteractionFactory.phpapp-modules/activity/database/factories/MessageFactory.phpapp-modules/activity/database/migrations/2026_03_18_000000_create_interactions_table.phpapp-modules/activity/routes/message-routes.phpapp-modules/activity/src/Message/Actions/NewMessage.phpapp-modules/activity/src/Message/Actions/PersistMessage.phpapp-modules/activity/src/Message/DTOs/NewMessageDTO.phpapp-modules/activity/src/Message/Http/Controllers/MessagesController.phpapp-modules/activity/src/Message/Http/Requests/CreateMessageRequest.phpapp-modules/activity/src/Message/Models/Message.phpapp-modules/activity/src/Providers/ActivityServiceProvider.phpapp-modules/activity/src/Tracking/Actions/ApproveInteraction.phpapp-modules/activity/src/Tracking/Actions/CalculateReward.phpapp-modules/activity/src/Tracking/Actions/ClassifyActivity.phpapp-modules/activity/src/Tracking/Actions/RejectInteraction.phpapp-modules/activity/src/Tracking/Actions/TrackActivity.phpapp-modules/activity/src/Tracking/Concerns/HasInteractions.phpapp-modules/activity/src/Tracking/Contracts/ActivitySourceContract.phpapp-modules/activity/src/Tracking/DTOs/TrackActivityDTO.phpapp-modules/activity/src/Tracking/Enums/ActivityStatus.phpapp-modules/activity/src/Tracking/Enums/ActivityType.phpapp-modules/activity/src/Tracking/Enums/ValueTier.phpapp-modules/activity/src/Tracking/Events/InteractionApproved.phpapp-modules/activity/src/Tracking/Events/InteractionTracked.phpapp-modules/activity/src/Tracking/Models/Interaction.phpapp-modules/activity/src/Voice/Actions/NewVoiceMessage.phpapp-modules/activity/src/Voice/DTOs/NewVoiceMessageDTO.phpapp-modules/activity/src/Voice/Http/Requests/CreateVoiceMessageRequest.phpapp-modules/activity/src/Voice/Models/Voice.phpapp-modules/activity/tests/Unit/Actions/NewMessageTest.phpapp-modules/activity/tests/Unit/Tracking/ApproveInteractionTest.phpapp-modules/activity/tests/Unit/Tracking/CalculateRewardTest.phpapp-modules/activity/tests/Unit/Tracking/ClassifyActivityTest.phpapp-modules/activity/tests/Unit/Tracking/RejectInteractionTest.phpapp-modules/activity/tests/Unit/Tracking/TrackActivityTest.phpapp-modules/bot-discord/src/Events/MessageReceivedEvent.phpapp-modules/gamification/src/Character/Models/Character.phpapp-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.phpapp-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.phpapp-modules/identity/src/Tenant/Models/Tenant.phpapp-modules/identity/tests/Feature/FindProfileTest.phpapp-modules/identity/tests/Feature/UpdateProfileTest.phpapp-modules/integration-devto/composer.jsonapp-modules/integration-devto/config/integration-devto.phpapp-modules/integration-devto/src/OAuth/DevToOAuthAccessDTO.phpapp-modules/integration-devto/src/OAuth/DevToOAuthClient.phpapp-modules/integration-devto/src/OAuth/DevToOAuthUser.phpapp-modules/integration-devto/src/Polling/DevToApiClient.phpapp-modules/integration-devto/src/Polling/SyncDevToArticles.phpapp-modules/integration-devto/src/Providers/IntegrationDevToServiceProvider.phpapp-modules/integration-devto/tests/Feature/DevToOAuthTest.phpapp-modules/integration-devto/tests/Feature/SyncDevToArticlesTest.php
✅ Files skipped from review due to trivial changes (22)
- app-modules/identity/src/Tenant/Models/Tenant.php
- app-modules/identity/tests/Feature/UpdateProfileTest.php
- app-modules/bot-discord/src/Events/MessageReceivedEvent.php
- app-modules/activity/src/Tracking/Contracts/ActivitySourceContract.php
- app-modules/activity/src/Tracking/Events/InteractionApproved.php
- app-modules/activity/routes/message-routes.php
- app-modules/activity/tests/Unit/Tracking/RejectInteractionTest.php
- app-modules/activity/src/Message/DTOs/NewMessageDTO.php
- app-modules/integration-devto/config/integration-devto.php
- app-modules/integration-devto/composer.json
- app-modules/activity/tests/Unit/Actions/NewMessageTest.php
- app-modules/activity/src/Message/Actions/PersistMessage.php
- app-modules/activity/src/Tracking/Enums/ActivityStatus.php
- app-modules/activity/src/Tracking/Enums/ValueTier.php
- app-modules/activity/src/Message/Http/Controllers/MessagesController.php
- app-modules/integration-devto/src/OAuth/DevToOAuthUser.php
- app-modules/activity/config/activity-tracking.php
- app-modules/activity/database/migrations/2026_03_18_000000_create_interactions_table.php
- app-modules/activity/tests/Unit/Tracking/ApproveInteractionTest.php
- app-modules/activity/src/Tracking/Enums/ActivityType.php
- app-modules/integration-devto/tests/Feature/SyncDevToArticlesTest.php
- app-modules/activity/src/Message/Models/Message.php
🚧 Files skipped from review as they are similar to previous changes (26)
- app-modules/activity/src/Tracking/Events/InteractionTracked.php
- app-modules/activity/src/Voice/Models/Voice.php
- app-modules/identity/tests/Feature/FindProfileTest.php
- app-modules/activity/database/factories/MessageFactory.php
- app-modules/activity/src/Message/Http/Requests/CreateMessageRequest.php
- app-modules/activity/src/Tracking/Concerns/HasInteractions.php
- app-modules/activity/src/Voice/Actions/NewVoiceMessage.php
- app-modules/activity/src/Tracking/Actions/RejectInteraction.php
- app-modules/activity/src/Voice/DTOs/NewVoiceMessageDTO.php
- app-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.php
- app-modules/integration-devto/src/OAuth/DevToOAuthAccessDTO.php
- app-modules/integration-devto/src/Providers/IntegrationDevToServiceProvider.php
- app-modules/activity/tests/Unit/Tracking/ClassifyActivityTest.php
- app-modules/activity/src/Voice/Http/Requests/CreateVoiceMessageRequest.php
- app-modules/activity/src/Tracking/Actions/ApproveInteraction.php
- app-modules/integration-devto/tests/Feature/DevToOAuthTest.php
- app-modules/activity/tests/Unit/Tracking/TrackActivityTest.php
- app-modules/activity/database/factories/InteractionFactory.php
- app-modules/integration-devto/src/Polling/DevToApiClient.php
- app-modules/activity/src/Tracking/Actions/ClassifyActivity.php
- app-modules/activity/tests/Unit/Tracking/CalculateRewardTest.php
- app-modules/integration-devto/src/OAuth/DevToOAuthClient.php
- app-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.php
- app-modules/integration-devto/src/Polling/SyncDevToArticles.php
- app-modules/activity/src/Message/Actions/NewMessage.php
- app-modules/activity/src/Providers/ActivityServiceProvider.php
Add generic types to HasFactory, specify iterable value types for metadata arrays, and remove unused Wallet import from Character model.
Summary
Trackingsubdomain withInteractionmodel for message/voice activity trackingHasInteractionstrait toCharactermodelDevToidentity provider and DevTo OAuth integration with article syncCommits
89f2385feat(gamification): add HasInteractions trait to Character modele1215c1feat(identity): add DevTo to IdentityProvider enum32e815bfeat(integration-devto): add new module for DevTo OAuth and article sync644964bfeat(activity): add Tracking subdomain with Interaction modelc49427erefactor(activity): restructure module into Message/Voice subdomainsSummary by CodeRabbit
New Features
Refactor