Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app-modules/activity/src/Retrospective/DiscordSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public function key(): string
return 'discord';
}

public function label(): string
{
return 'Discord';
}

public function collect(Period $period, SourceFilters $filters): SourceResult
{
$totalMessages = $this->messages($period, $filters)->count();
Expand Down Expand Up @@ -73,7 +78,7 @@ public function collect(Period $period, SourceFilters $filters): SourceResult

return new SourceResult(
key: $this->key(),
label: 'Discord',
label: $this->label(),
headline: $this->headline($totalMessages, $participants, $joins, $totalReactions),
slides: $this->slides(
participants: $participants,
Expand Down
49 changes: 49 additions & 0 deletions app-modules/community/database/factories/RetrospectiveFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace He4rt\Community\Database\Factories;

use Carbon\CarbonImmutable;
use He4rt\Community\Retrospective\DTOs\DeckConfig;
use He4rt\Community\Retrospective\DTOs\RetrospectiveSnapshot;
use He4rt\Community\Retrospective\Enums\RetrospectiveStatus;
use He4rt\Community\Retrospective\Models\Retrospective;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends Factory<Retrospective>
*/
final class RetrospectiveFactory extends Factory
{
protected $model = Retrospective::class;

public function definition(): array
{
$since = CarbonImmutable::now()->subMonth()->startOfDay();

return [
'id' => fake()->uuid(),
'title' => 'Retrospectiva de '.fake()->monthName(),
'since' => $since,
'until' => CarbonImmutable::now(),
'status' => RetrospectiveStatus::Draft,
'cover_title' => fake()->sentence(3),
'cover_intro' => fake()->sentence(),
'closing_text' => fake()->sentence(),
'hide_bots' => true,
'deck_config' => new DeckConfig(),
'snapshot' => null,
'published_at' => null,
];
}

public function published(?RetrospectiveSnapshot $snapshot = null): self
{
return $this->state(fn (): array => [
'status' => RetrospectiveStatus::Published,
'published_at' => CarbonImmutable::now(),
'snapshot' => $snapshot ?? new RetrospectiveSnapshot(),
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

use He4rt\Community\Retrospective\Enums\RetrospectiveStatus;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::create('community_retrospectives', static function (Blueprint $table): void {
$table->uuid('id')->primary();
$table->string('title');
$table->timestampTz('since');
$table->timestampTz('until');
$table->string('status', 20)->default(RetrospectiveStatus::Draft->value)->comment(RetrospectiveStatus::stringifyCases());
$table->string('cover_title')->nullable();
$table->text('cover_intro')->nullable();
$table->text('closing_text')->nullable();
$table->boolean('hide_bots')->default(value: true);
// Curadoria de apresentação (ordem, on/off, exclusions). Cast AsDeckConfig
// normaliza null para um DeckConfig vazio na leitura.
$table->jsonb('deck_config')->nullable();
// SourceResult[] congelados no publish; null enquanto rascunho.
$table->jsonb('snapshot')->nullable();
$table->timestampTz('published_at')->nullable();
$table->timestampsTz();

// Página pública busca a edição publicada mais recente.
$table->index(['status', 'published_at']);
});
}

public function down(): void
{
Schema::dropIfExists('community_retrospectives');
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,25 @@ O custo é proporcional à janela de datas, não ao total, porque há índice na
`snapshot + config`), então "ver rascunho" bate com o publicado.
- **Divergência consciente da spec-base:** ela era toda "per tenant" e centrada em `Cadence`. Aqui não há
tenancy (removida no #413) e o período é datas livres.

## Notas de implementação (Fase 2)

Detalhes que emergiram na implementação, dentro do que foi decidido acima:

- **`FrozenSlide` em vez de registro `kind -> classe`.** O snapshot reidrata cada slide como um `Slide`
genérico (`FrozenSlide`, dono no `community`, carrega `kind` + props). Um registro `kind -> classe`
exigiria o domínio `community` conhecer as classes de slide de `integration-github` (Domain -> Integration,
proibido). Como a renderização é por convenção `kind -> Blade` sobre `toArray()`, o contrato `Slide`
basta — o tipo concreto do slide só importa na produção do dado (no `collect()`), não no consumo do
snapshot.
- **`label()` no contrato `RetrospectiveSource`.** A identidade da fonte (key + label) virou estática para
o CRUD listar/ordenar as fontes sem coletar dado. Antes o label era hardcoded dentro do `collect()`.
- **Exclusions recompilam; ordem/on-off re-derivam.** Ordem e on/off (fonte/slide) são curadoria de
apresentação: aplicadas na composição (`ComposeDeck`) sobre o snapshot congelado, baratas. Exclusions
mexem no DADO (entram no `SourceFilters` do `collect`, ADR-0001), então alterá-las só reflete numa nova
publicação (recompila o snapshot) — não são reaplicadas na composição.
- **`publishing` é um estado transitório do enum.** `RetrospectiveStatus` tem `draft | publishing |
published`; `publishing` cobre a janela em que o job congela o snapshot.
- **Preview autenticado sem rota de login web.** O portal não tem rota `login` nomeada, então o guard do
preview vive no `mount()` do componente (`abort_unless(auth()->check(), 403)`), não num middleware `auth`
que dependeria da rota inexistente.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Partimos de uma spec-base que foi destrinchada contra o projeto de hoje. Três p
```
interface RetrospectiveSource {
key(): string
label(): string // identidade estática (CRUD lista/ordena sem coletar)
collect(Period, SourceFilters): SourceResult
}

Expand Down Expand Up @@ -114,7 +115,10 @@ Retrospective (community · tabela community_retrospectives)
```

`deck_config` e `snapshot` são VOs tipados via cast dedicado (regra de typed-json do repo), nunca `array`
solto. A reidratação dos `Slide` tipados usa um registro `kind -> classe` no cast do snapshot.
solto. Ao reidratar o snapshot, cada slide volta como um `FrozenSlide` (Slide genérico do `community`
que carrega `kind` + props) em vez de reinstanciar a classe concreta: o domínio `community` não pode
importar as classes de slide de `integration-github` (Domain -> Integration é proibido), e a renderização
por convenção `kind -> Blade` só precisa do contrato `Slide`, não do tipo original.

### Slides e shell

Expand Down
16 changes: 15 additions & 1 deletion app-modules/community/src/CommunityServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@

namespace He4rt\Community;

use He4rt\Community\Retrospective\Actions\CompileSnapshot;
use He4rt\Community\Retrospective\Contracts\RetrospectiveSource;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;

class CommunityServiceProvider extends ServiceProvider
{
public function register(): void {}
public function register(): void
{
// Resolve as fontes descobertas por tag no boundary do container; o
// domínio depende só do contrato (dele), nunca das implementações
// concretas em integration-github/activity.
$this->app->bind(CompileSnapshot::class, static function (Application $app): CompileSnapshot {
/** @var iterable<RetrospectiveSource> $sources */
$sources = $app->tagged('retrospective.source');

return new CompileSnapshot($sources);
});
}

public function boot(): void
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace He4rt\Community\Retrospective\Actions;

use He4rt\Community\Retrospective\Contracts\RetrospectiveSource;
use He4rt\Community\Retrospective\DTOs\Period;
use He4rt\Community\Retrospective\DTOs\RetrospectiveSnapshot;
use He4rt\Community\Retrospective\DTOs\SourceFilters;

/**
* Coleta todas as fontes registradas para um Period + filtros e empacota o
* resultado cru num RetrospectiveSnapshot. É o que o publish congela.
*
* Não ordena nem cura: a ordem e o on/off vivem no DeckConfig e são aplicados
* depois pelo ComposeDeck. Só descarta fontes vazias (não há o que congelar).
* Adicionar uma fonte não toca esta classe: basta a tag "retrospective.source".
*/
final readonly class CompileSnapshot
{
/** @var list<RetrospectiveSource> */
private array $sources;

/**
* @param iterable<RetrospectiveSource> $sources
*/
public function __construct(iterable $sources)
{
$this->sources = array_values(
is_array($sources) ? $sources : iterator_to_array($sources, preserve_keys: false),
);
}

public function execute(Period $period, SourceFilters $filters): RetrospectiveSnapshot
{
$results = [];

foreach ($this->sources as $source) {
$result = $source->collect($period, $filters);

if (!$result->isEmpty()) {
$results[] = $result;
}
}

return new RetrospectiveSnapshot($results);
}
}
60 changes: 60 additions & 0 deletions app-modules/community/src/Retrospective/Actions/ComposeDeck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace He4rt\Community\Retrospective\Actions;

use He4rt\Community\Retrospective\Contracts\Slide;
use He4rt\Community\Retrospective\DTOs\DeckConfig;
use He4rt\Community\Retrospective\DTOs\RetrospectiveSnapshot;
use He4rt\Community\Retrospective\DTOs\SourceResult;

/**
* Compositor único do deck: aplica a curadoria de apresentação (DeckConfig) sobre
* um snapshot já coletado, sem tocar as fontes. É o render path compartilhado
* entre a página pública (snapshot congelado) e o preview do admin (snapshot ao
* vivo) — "ver rascunho" bate com o publicado (ADR-0002).
*
* Ordem e on/off (fonte/slide) re-derivam do snapshot aqui, baratos. Exclusions
* NÃO são reaplicadas: elas mexem no dado e já entraram no collect que gerou o
* snapshot (ADR-0001).
*/
final readonly class ComposeDeck
{
/**
* @return list<SourceResult>
*/
public function execute(RetrospectiveSnapshot $snapshot, DeckConfig $config): array
{
$sources = [];

foreach ($snapshot->sources as $source) {
if (!$config->showsSource($source->key)) {
continue;
}

$composed = $this->applySlideVisibility($source, $config);

if (!$composed->isEmpty()) {
$sources[] = $composed;
}
}

usort(
$sources,
static fn (SourceResult $a, SourceResult $b): int => $config->position($a->key) <=> $config->position($b->key),
);

return $sources;
}

private function applySlideVisibility(SourceResult $source, DeckConfig $config): SourceResult
{
$slides = array_values(array_filter(
$source->slides,
static fn (Slide $slide): bool => $config->showsSlide($slide->kind()),
));

return new SourceResult($source->key, $source->label, $source->headline, $slides);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace He4rt\Community\Retrospective\Actions;

use He4rt\Community\Retrospective\Enums\RetrospectiveStatus;
use He4rt\Community\Retrospective\Jobs\CompileRetrospectiveSnapshot;
use He4rt\Community\Retrospective\Models\Retrospective;

/**
* Publica (ou republica) uma edição. Marca "publicando" e despacha o job que
* congela o snapshot em segundo plano — coletar todas as fontes sobre a janela
* pode ser pesado num range anual e estourar o timeout do request (ADR-0002).
*/
final readonly class PublishRetrospective
{
public function execute(Retrospective $retrospective): void
{
$retrospective->forceFill(['status' => RetrospectiveStatus::Publishing])->save();

dispatch(new CompileRetrospectiveSnapshot($retrospective));
}
}
36 changes: 36 additions & 0 deletions app-modules/community/src/Retrospective/Casts/AsDeckConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace He4rt\Community\Retrospective\Casts;

use He4rt\Community\Retrospective\DTOs\DeckConfig;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

/**
* @implements CastsAttributes<DeckConfig, DeckConfig|array<string, mixed>>
*/
final class AsDeckConfig implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): DeckConfig
{
$payload = json_decode((string) ($value ?? '{}'), associative: true);

return DeckConfig::makeFromPayload(is_array($payload) ? $payload : []);
}

/**
* @param DeckConfig|array<string, mixed>|null $value
*/
public function set(Model $model, string $key, mixed $value, array $attributes): string
{
$config = match (true) {
$value instanceof DeckConfig => $value,
is_array($value) => DeckConfig::makeFromPayload($value),
default => new DeckConfig(),
};

return json_encode($config->toArray(), JSON_THROW_ON_ERROR);
}
}
Loading