-
Notifications
You must be signed in to change notification settings - Fork 13
Description
19 modules in app-modules/ grew organically. Circular dependencies exist between auth modules, hub models import from 6+ modules, trivially thin modules waste cognitive overhead, and Filament presentation code is mixed into domain modules. This reorganization separates concerns into clear bounded contexts, standardizes on Actions over Repositories/Services, and splits Filament panels into independent modules.
Current State: 19 Modules
authentication badge bot-discord character docs events feedback
he4rt integrations meeting message portal provider ranking
season shared sponsors tenant user
Problems
- Circular dependency:
authentication<->integrations<->provider—OAuthProviderEnumimports fromintegrations,integrationsimports fromauthentication - God Objects:
Tenantimports from 6 modules,Userfrom 4 external modules - Filament mixed into domain: 95+ Filament files scattered across 10 modules — presentation logic entangled with business logic
- Trivially thin modules:
ranking(1 action, 0 tables),portal(1 page),sponsors(only relates to events) - Fragmented identity: User, tenant, auth, provider, integrations — 5 modules for one concern
- Monolithic integrations: Discord OAuth + Twitch OAuth bundled together despite being independent providers
- Over-engineered abstractions: Repositories (Contract + Eloquent impl), Services, Entities, Collections — unnecessary layers in 10+ modules when Actions + Models suffice
- All panels in one blob: A single
panelsmodule would still create merge conflicts and unclear ownership across 5 different panels
Architectural Changes
1. Actions Replace Repositories, Services, and Entities
Current state: Modules use a mix of Repository (Contract + Eloquent impl), Service classes, Entity value objects, and Collections — creating 4-5 abstraction layers between a use case and the database.
New standard: Actions are the only use-case abstraction. They use Eloquent Models directly. No Repositories, no Services, no Entities, no Collections wrappers.
Action Convention
declare(strict_types=1);
namespace He4rt\Gamification\Character\Actions;
use He4rt\Gamification\Character\DTOs\IncrementExperienceDTO;
use He4rt\Gamification\Character\Models\Character;
final class IncrementExperience
{
public function __construct(
private Character $character,
) {}
public function handle(IncrementExperienceDTO $dto): void
{
$this->character->increment('experience', $dto->amount);
// business logic...
}
}Rules:
- Method:
handle()(not__invoke) - Constructor: inject Models, other Actions, or framework services
- Parameters: always DTOs for input data (never loose primitives)
- Interfaces: only where a genuine abstraction is needed (e.g.,
OAuthClientContractfor multiple identity providers) - Query-only "actions" (no validation, no exception, no side effects): inline at point of use — just use
Model::query()directly. Don't create an Action forBadge::findOrFail($id).
What Gets Deleted Per Module
| Layer | Example | Replacement |
|---|---|---|
Contracts/CharacterRepository.php |
Interface for repository | Delete. Use Model directly in Actions |
Repositories/CharacterEloquentRepository.php |
Eloquent implementation | Delete. Model queries live in Actions or inline |
Entities/CharacterEntity.php |
Value object wrapping model data | Delete. Use Model attributes directly |
Collections/PastSeasonCollection.php |
Custom collection wrapper | Delete. Use Eloquent Collection |
Services/UpdateProfileService.php |
Orchestration class | Delete. Becomes an Action |
ValueObjects/UserId.php |
Typed ID wrapper | Delete unless genuinely needed for type safety |
Modules Affected
| Module | Has Repositories | Has Services | Has Entities | Has Collections |
|---|---|---|---|---|
| badge | yes | - | yes | yes |
| character | yes | - | yes | yes |
| feedback | yes | - | yes | - |
| meeting | yes | - | yes | - |
| message | yes | - | yes | - |
| provider | yes | - | yes | - |
| ranking | yes | - | - | - |
| season | yes | - | yes | yes |
| user | yes | yes | yes | - |
2. shared Module Dies — app/ Becomes Core
Current state: shared module contains Paginator, IntValueObject, TTL, and a contract.
New state: Inline these into app/ or the consuming module:
| Shared item | Destination |
|---|---|
Contract/Paginator.php |
app/Contracts/Paginator.php |
ValueObjects/IntValueObject.php |
Inline into consuming module or delete |
TTL.php |
Inline into consuming module or delete |
Factory.php |
Inline into consuming module or delete |
Paginator.php |
app/Support/Paginator.php |
Cross-module contracts (like OAuthClientContract) also live in app/Contracts/.
3. Panels Split Into Independent Modules
Current state (proposed single panels): One module with 95+ files across 5 panels — same merge conflict problem we're trying to solve.
New state: Each panel is its own module under app-modules/panel-*. This gives each panel its own ServiceProvider, tests, and clear ownership.
app-modules/
├── panel-admin/ # Filament Admin panel (/admin)
├── panel-app/ # Filament User panel (/app)
├── panel-guest/ # Blade + Livewire public site (/)
├── panel-event/ # Filament Event panel (/event)
└── panel-partner/ # Filament Partner panel (/partner)
PanelProviders stay in app/Providers/Filament/ — they are not moved into the modules. The modules contain Resources, Pages, Widgets, and Schemas; the PanelProviders in app/ wire them together.
Shared components (Login page, reusable schemas, cross-panel utilities) live in app/Filament/Shared/ or app/Livewire/.
panel-guest uses no Filament — it's pure Blade + Livewire with its own routes in panel-guest/routes/web.php.
Proposed Structure: 19 -> 16 Modules
app-modules/
├── identity/ # user + tenant + auth + external identity (socialite)
├── gamification/ # character + badge + ranking + season
├── economy/ # wallets, currencies, transactions
├── events/ # events + sponsors
├── community/ # meeting + feedback
├── activity/ # message + voice tracking (multi-provider)
├── integration-discord/ # Discord OAuth client
├── integration-twitch/ # Twitch OAuth + subscriber
├── bot-discord/ # Laracord bot (commands, events, tasks)
├── he4rt/ # CSS/design system only
├── panel-admin/ # Filament admin panel
├── panel-app/ # Filament user panel
├── panel-guest/ # Blade + Livewire public site
├── panel-event/ # Filament event panel
└── panel-partner/ # Filament partner panel
app/ (core):
app/
├── Contracts/ # Cross-module interfaces (OAuthClientContract, Paginator)
├── Support/ # Shared utilities (Paginator impl, etc.)
├── Enums/ # FilamentPanel enum
├── Filament/
│ └── Shared/ # Cross-panel components (Login, reusable schemas)
├── Providers/
│ ├── AppServiceProvider.php
│ ├── FilamentServiceProvider.php
│ └── Filament/
│ ├── AdminPanelProvider.php
│ ├── UserPanelProvider.php
│ ├── GuestPanelProvider.php # may become unnecessary if panel-guest has no Filament
│ ├── EventPanelProvider.php
│ └── PartnerPanelProvider.php
├── Http/
├── Livewire/
└── Rules/
Domain Modules
1. identity (merge: user + tenant + authentication + provider)
All IAM concerns: who you are, what orgs you belong to, how you authenticate.
| What | Details |
|---|---|
| Namespace | He4rt\Identity |
| Sub-namespaces | User, Tenant, Auth, ExternalIdentity |
| Tables | users, user_address, user_information, sessions, tenants, tenant_users, external_identities, external_identity_tokens, password_resets, personal_access_tokens |
| Key models | User, Address, Information, Tenant, ExternalIdentity, AccessToken |
| Key actions | ResolveUserContext, LinkExternalIdentity, UpdateProfile |
| DTOs | UpdateProfileDTO, UpsertInformationDTO, UserContextDTO, ResolveExternalIdentityDTO |
Rename: Provider -> ExternalIdentity
The current Provider model represents an external identity linked to a User or Tenant (polymorphic). It stores the platform user ID, username, avatar, and OAuth tokens. It is also used for activity attribution — messages and voice records reference it to track which external identity generated the interaction.
| Current | New | Reason |
|---|---|---|
Provider (model) |
ExternalIdentity |
Formal IAM term — the identity that comes from an external platform |
Token (model) |
AccessToken |
More explicit — it's an OAuth access/refresh token pair |
ProviderEnum (enum) |
IdentityProvider |
Discord and Twitch are identity providers |
providers (table) |
external_identities |
Follows model rename |
provider_tokens (table) |
external_identity_tokens |
Follows model rename |
provider_id (FK in messages/voice) |
external_identity_id |
FK to external_identities |
provider (column, enum cast) |
identity_provider |
Which platform (IdentityProvider enum) |
provider_id (column, platform user ID) |
platform_user_id |
The user/guild ID on the external platform |
ProviderResolver (action) |
ResolveExternalIdentity |
Follows domain rename |
FindProvider (action) |
Inline at point of use | Query-only, no validation/exception |
NewAccountByProvider (action) |
LinkExternalIdentity |
Clearer intent |
Current files being merged:
app-modules/user/src/->app-modules/identity/src/User/app-modules/tenant/src/->app-modules/identity/src/Tenant/app-modules/authentication/src/->app-modules/identity/src/Auth/app-modules/provider/src/->app-modules/identity/src/ExternalIdentity/
Deleted in the process:
user/Repositories/,user/Contracts/,user/Entities/,user/Services/,user/ValueObjects/provider/Repositories/,provider/Contracts/,provider/Entities/,provider/ValueObjects/- All Filament code ->
panel-admin/,panel-app/ - All Plugins -> deleted (panel modules register their own resources)
2. gamification (merge: character + badge + ranking + season)
XP, levels, badges, seasons, leaderboards — one domain.
| What | Details |
|---|---|
| Namespace | He4rt\Gamification |
| Sub-namespaces | Character, Badge, Season |
| Tables | characters, characters_badges, characters_leveling_logs, badges, seasons, seasons_rankings |
| Key models | Character, PastSeason, Badge, Season |
| Key actions | InitializeCharacter, IncrementExperience, ClaimBadge, PersistDailyBonus, ManageReputation |
| Absorbs | ranking entirely (1 action inlined at point of use) |
Deleted:
character/Repositories/,character/Contracts/,character/Entities/,character/Collections/badge/Repositories/,badge/Contracts/,badge/Entities/,badge/Collections/season/Repositories/,season/Contracts/,season/Entities/,season/Collections/ranking/entirely- All Filament code ->
panel-admin/
3. economy (new module, extracts Wallet from character)
Internal economy: wallets, currencies, and transactions. Supports polymorphic ownership (User, Character, Tenant).
The current characters_wallet table is tightly coupled to Character. The new economy module replaces it with a generic, polymorphic wallet system that any entity can own — enabling tenant-scoped economies where members transact with each other.
| What | Details |
|---|---|
| Namespace | He4rt\Economy |
| Tables | wallets, transactions |
| Key models | Wallet, Transaction |
| Key actions | CreateWallet, Credit, Debit, Transfer |
| DTOs | CreditDTO, DebitDTO, TransferDTO |
| Enums | Currency, TransactionType |
Currency enum (system-wide, not per-tenant):
enum Currency: string
{
case Coin = 'coin'; // He4rt Coin — gamification rewards, community engagement
case Cash = 'cash'; // Monetary representation — future store purchases
}Tenants can customize the label of each currency (e.g., "He4rt Coin" -> "Dev Moeda") but the currency itself is global.
Wallet model (polymorphic):
// wallets table
Schema::create('wallets', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuidMorphs('owner'); // owner_type + owner_id (User, Character, Tenant)
$table->string('currency'); // Currency enum
$table->bigInteger('balance')->default(0);
$table->timestamps();
$table->unique(['owner_type', 'owner_id', 'currency']);
});Transaction model (immutable ledger):
// transactions table
Schema::create('transactions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('wallet_id')->constrained('wallets');
$table->string('type'); // TransactionType enum
$table->bigInteger('amount'); // positive = credit, negative = debit
$table->bigInteger('balance_after'); // snapshot of wallet balance after transaction
$table->nullableUuidMorphs('reference'); // polymorphic ref to what caused it (Event, Badge, etc.)
$table->string('description')->nullable();
$table->timestamps();
});TransactionType enum:
enum TransactionType: string
{
case Reward = 'reward'; // System -> User (event participation, badge claim, etc.)
case Transfer = 'transfer'; // User -> User (P2P between members)
case Purchase = 'purchase'; // User -> System (store redemption)
}Transaction flows:
System reward (event participation):
events dispatches -> EventAttended { user_id, event_id, tenant_id }
economy listens -> Credit wallet, create Transaction(type: reward, reference: event)
P2P transfer:
panel-app action -> Transfer
economy -> Debit sender wallet, Credit receiver wallet, create 2 Transactions(type: transfer)
Store purchase (future):
panel-app action -> Purchase
economy -> Debit wallet, create Transaction(type: purchase, reference: product)
Key rules:
- Wallets cannot have negative balance — Debit/Transfer actions validate before executing
- All balance changes go through Actions that create a Transaction record (no direct
$wallet->update()) balance_afteron Transaction ensures auditability without recalculating from history- Communication with other modules via Laravel Events (economy listens, never called directly)
Migration from characters_wallet:
- Create new
walletsandtransactionstables - Migrate existing
characters_walletbalances towalletswithowner_type: Character,currency: coin - Drop
characters_wallettable
4. events (merge: events + sponsors)
Event management + sponsorship — sponsors only exist in relation to events.
| What | Details |
|---|---|
| Namespace | He4rt\Events |
| Tables | events, events_attendees, events_talks, events_agenda, event_submission_speakers, sponsors, events_sponsors |
| Key models | EventModel, EventSubmission, EventAgenda, Sponsor |
Deleted:
- All Filament code ->
panel-admin/,panel-app/,panel-event/
5. community (merge: meeting + feedback)
Tenant-scoped community engagement features.
| What | Details |
|---|---|
| Namespace | He4rt\Community |
| Sub-namespaces | Meeting, Feedback |
| Tables | meetings, meeting_types, meeting_participants, feedbacks, feedback_reviews |
| Key models | Meeting, MeetingType, Feedback, Review |
Deleted:
meeting/Repositories/,meeting/Contracts/,meeting/Entities/feedback/Repositories/,feedback/Contracts/,feedback/Entities/- All Filament code ->
panel-admin/
6. activity (rename: message)
Multi-provider activity tracking: messages, voice, XP attribution.
| What | Details |
|---|---|
| Namespace | He4rt\Activity |
| Tables | messages, voice_messages |
| Key models | Message, Voice |
| Key actions | NewMessage, NewVoiceMessage, PersistMessage |
| DTOs | NewMessageDTO, NewVoiceMessageDTO |
Deleted:
message/Repositories/,message/Contracts/,message/Entities/- All Filament code ->
panel-admin/
7. integration-discord (split from: integrations)
Transport layer: Discord HTTP/OAuth client. No domain logic.
| What | Details |
|---|---|
| Namespace | He4rt\IntegrationDiscord |
| Tables | none |
| Contains | DiscordOAuthClient (implements OAuthClientContract from app/Contracts/), Discord OAuth DTOs |
8. integration-twitch (split from: integrations)
Transport layer: Twitch HTTP/OAuth client + subscriber API. No domain logic.
| What | Details |
|---|---|
| Namespace | He4rt\IntegrationTwitch |
| Tables | none |
| Contains | TwitchOAuthClient, TwitchSubscriberClient, subscriber DTOs |
9. bot-discord (stays)
Laracord bot. Catches Discord events, dispatches Laravel events.
10. he4rt (stays, CSS only)
Design system: custom Blade components, fonts, CSS themes.
Panel Modules
11. panel-admin — Filament Admin (/admin)
Full admin panel. All CRUD resources for all domain modules.
| What | Details |
|---|---|
| Namespace | He4rt\PanelAdmin |
| Tables | none |
| Depends on | identity, gamification, economy, events, community, activity |
app-modules/panel-admin/src/
├── Resources/
│ ├── Users/ # from identity
│ │ ├── UserResource.php
│ │ ├── Pages/
│ │ ├── Schemas/
│ │ └── Tables/
│ ├── Tenants/ # from identity
│ ├── Badges/ # from gamification
│ ├── Seasons/ # from gamification
│ ├── Wallets/ # from economy
│ ├── Transactions/ # from economy
│ ├── Events/ # from events
│ ├── Talks/ # from events
│ ├── EventAgenda/ # from events
│ ├── Sponsors/ # from events
│ ├── Meetings/ # from community
│ ├── MeetingTypes/ # from community
│ ├── Feedback/ # from community
│ └── Messages/ # from activity
├── RelationManagers/
│ ├── AttendeesRelationManager.php
│ ├── TalksRelationManager.php
│ ├── EventsRelationManager.php
│ └── MembersRelationManager.php
├── Widgets/
│ ├── SeasonStatsOverview.php
│ ├── UsersStatsOverview.php
│ └── ActiveEventsStats.php
├── Providers/
│ └── PanelAdminServiceProvider.php
└── tests/
12. panel-app — Filament User Panel (/app)
Authenticated user dashboard, profile, tenant-scoped features.
| What | Details |
|---|---|
| Namespace | He4rt\PanelApp |
| Tables | none |
| Depends on | identity, gamification, economy, events |
app-modules/panel-app/src/
├── Pages/
│ ├── Dashboard.php
│ └── UserProfile.php
├── Resources/
│ ├── EventModels/ # user-facing event list
│ └── Talks/ # user-facing talk submissions
├── Widgets/
│ └── LatestEvents.php
├── Schemas/
│ ├── UserInformationForm.php
│ └── UserAddressForm.php
├── Providers/
│ └── PanelAppServiceProvider.php
└── tests/
13. panel-guest — Blade + Livewire Public Site (/)
No Filament. Pure Blade + Livewire with own routes and controllers.
| What | Details |
|---|---|
| Namespace | He4rt\PanelGuest |
| Tables | none |
| Depends on | identity, events |
| Absorbs | portal + docs modules entirely |
app-modules/panel-guest/
├── src/
│ ├── Http/
│ │ └── Controllers/
│ ├── Livewire/
│ │ └── Components/
│ └── Providers/
│ └── PanelGuestServiceProvider.php
├── routes/
│ └── web.php
├── resources/
│ └── views/
└── tests/
14. panel-event — Filament Event Panel (/event)
Per-tenant event landing pages and participant dashboards.
| What | Details |
|---|---|
| Namespace | He4rt\PanelEvent |
| Tables | none |
| Depends on | identity, events |
app-modules/panel-event/src/
├── Pages/
│ ├── EventLandingPage.php
│ └── ParticipantDashboard.php
├── Schemas/
│ └── StartEndFieldsSchema.php
├── Providers/
│ └── PanelEventServiceProvider.php
└── tests/
15. panel-partner — Filament Partner Panel (/partner)
| What | Details |
|---|---|
| Namespace | He4rt\PanelPartner |
| Tables | none |
| Depends on | identity, events |
app-modules/panel-partner/src/
├── Resources/
│ └── ...
├── Providers/
│ └── PanelPartnerServiceProvider.php
└── tests/
Module Standard Structure
After refactoring, every domain module follows this standard:
app-modules/{module}/
├── src/
│ ├── Actions/ # Use cases (handle() method, DTO params)
│ ├── DTOs/ # Input/output data transfer objects
│ ├── Enums/ # Domain enums
│ ├── Exceptions/ # Domain exceptions
│ ├── Models/ # Eloquent models (direct usage, no wrapper)
│ ├── Observers/ # Model observers (if needed)
│ └── Providers/
│ └── {Module}ServiceProvider.php
├── database/
│ └── migrations/
├── routes/
├── config/
└── tests/
NOT allowed in domain modules:
Repositories/— use Model directly in ActionsContracts/— useapp/Contracts/for cross-module interfacesEntities/— use Model attributes directlyCollections/— use Eloquent CollectionServices/— becomes an ActionFilament/— lives inpanel-*modulesPlugins/— panel modules register their own resources
Every panel module follows this standard:
app-modules/panel-{name}/
├── src/
│ ├── Resources/ # Filament resources (with Pages/, Schemas/, Tables/)
│ ├── Pages/ # Standalone Filament pages
│ ├── Widgets/ # Filament widgets
│ ├── Schemas/ # Reusable form schemas
│ ├── RelationManagers/ # Relation managers (if needed)
│ └── Providers/
│ └── Panel{Name}ServiceProvider.php
└── tests/
Plugin Registration Change
Current: Each domain module registers a Filament Plugin via Panel::configureUsing() in its ServiceProvider.
New: Panel modules register their own resources directly in their ServiceProvider. Domain modules have zero knowledge of Filament.
// BEFORE: app-modules/badge/src/Providers/BadgeServiceProvider.php
Panel::configureUsing(function (Panel $panel): void {
match ($panel->currentPanel()) {
FilamentPanel::Admin => $panel->plugin(new AdminBadgePanelPlugin),
default => null,
};
});
// AFTER: app-modules/panel-admin/src/Providers/PanelAdminServiceProvider.php
// Resources are auto-discovered or registered directly — no Plugin classes needed.
// Domain modules (badge, etc.) have no Filament code at all.Communication Architecture: Event-Driven
Current state: Zero Laravel events. All cross-module communication is direct action calls (tight coupling).
New principle: Integration modules are transport layers. Cross-module communication happens through Laravel Events.
Current (tightly coupled):
Discord API -> Laracord Event -> bot-discord/MessageReceivedEvent
-> directly calls message/NewMessage.persist()
-> directly calls character/IncrementExperience
-> directly calls user/ResolveUserContextService
Target (event-driven):
Discord API -> Laracord Event -> bot-discord/MessageReceivedEvent
-> dispatches Laravel Event: ActivityMessageReceived
-> activity module LISTENS -> persists message, calculates XP
-> gamification module LISTENS -> increments character experience
Key Event Flows
Message received (from any platform — Discord, Twitch, WhatsApp):
bot-discord dispatches -> ActivityMessageReceived { external_identity_id, channel_id, content, tenant_id }
activity listens -> PersistMessage
gamification listens -> IncrementExperience
Voice state change:
bot-discord dispatches -> ActivityVoiceStateChanged { external_identity_id, channel, state, tenant_id }
activity listens -> PersistVoiceMessage
gamification listens -> IncrementExperience (voice)
New member joins:
bot-discord dispatches -> MemberJoined { provider_data, tenant_id }
identity listens -> ResolveUserContext, LinkExternalIdentity, InitializeCharacter
Where Events Live
Events are defined by the dispatching module. Listeners are defined by the consuming module.
bot-discord/src/Events/Laravel/—ActivityMessageReceived,ActivityVoiceStateChanged,MemberJoinedactivity/src/Listeners/—OnActivityMessageReceived,OnActivityVoiceStateChangedgamification/src/Listeners/—OnActivityMessageReceived(XP calc),OnMemberJoined(char init)identity/src/Listeners/—OnMemberJoined(resolve user context)
Dependency Direction
he4rt <- CSS/views only, no domain deps
identity <- core domain (user, tenant, auth, external identity)
gamification <- depends on: identity (models only)
economy <- depends on: identity (models only, polymorphic owners)
activity <- depends on: identity (models only)
events <- depends on: identity (models only)
community <- depends on: identity (models only)
integration-discord <- transport layer, depends on: app/Contracts (OAuthClientContract)
integration-twitch <- transport layer, depends on: app/Contracts (OAuthClientContract)
bot-discord <- transport layer, dispatches events (minimal direct deps)
panel-admin <- depends on: identity, gamification, economy, events, community, activity
panel-app <- depends on: identity, gamification, economy, events
panel-guest <- depends on: identity, events (Blade + Livewire, no Filament)
panel-event <- depends on: identity, events
panel-partner <- depends on: identity, events
Key rules:
- Domain modules never import from
panel-*. Panel modules import from domain modules. integration-*modules are thin transport/HTTP clients. They don't call domain actions.bot-discordcatches Laracord (Discord.php) events and dispatches Laravel events. It does NOT directly call domain actions.- Domain modules communicate through Laravel events, not direct action imports.
- Domain modules may depend on
identityforUser/Tenantmodels (shared kernel entities). - Cross-module contracts live in
app/Contracts/, not in any module.
Table Ownership
| Module | Tables | Count |
|---|---|---|
identity |
users, user_address, user_information, sessions, tenants, tenant_users, external_identities, external_identity_tokens, password_resets, personal_access_tokens | 10 |
gamification |
characters, characters_badges, characters_leveling_logs, badges, seasons, seasons_rankings | 6 |
economy |
wallets, transactions | 2 |
events |
events, events_attendees, events_talks, events_agenda, event_submission_speakers, sponsors, events_sponsors | 7 |
community |
meetings, meeting_types, meeting_participants, feedbacks, feedback_reviews | 5 |
activity |
messages, voice_messages | 2 |
integration-discord |
— | 0 |
integration-twitch |
— | 0 |
bot-discord |
— | 0 |
panel-admin |
— | 0 |
panel-app |
— | 0 |
panel-guest |
— | 0 |
panel-event |
— | 0 |
panel-partner |
— | 0 |
he4rt |
— | 0 |
| Total | 32 (+6 infra) |