offload-project/laravel-waitlist

Convenções e APIs para o pacote offload-project/laravel-waitlist — múltiplas listas de espera, rastreamento de status de entrada, verificação opcional de e-mail, eventos de ciclo de vida, sincronização de listas de e-mail (Mailchimp/Kit/Audienceful) e ponte para laravel-invite-only.

O que é laravel-waitlist?

Este skill fornece convenções e APIs para gerenciar o pacote **offload-project/laravel-waitlist** em aplicações Laravel (11/12/13) com PHP 8.3+. Ele permite criar e gerenciar múltiplas listas de espera (waitlists) com modelos Eloquent (`Waitlist` e `WaitlistEntry`), um serviço central (`WaitlistService`) acessado via facade `Waitlist`, e fluxos como adição de entradas, verificação de e-mail (opcional), convite (via ponte com `laravel-invite-only`), e rejeição. O pacote oferece notificações customizáveis (`WaitlistInvited`, `VerifyWaitlistEmail`), eventos de ciclo de vida (como `WaitlistEntryAdded`, `WaitlistEntryVerified`) e integração com serviços de mailing list (Mailchimp, Kit, Audienceful) através de jobs em fila, com suporte a drivers personalizados via contrato `MailingListDriver`. Exceções tipadas (`UnverifiedEntryException`, `MailingListException`) facilitam o tratamento de erros. O skill documenta regras claras de uso, incluindo a preferência pela facade em vez de instanciação direta, uso correto de `for()` para selecionar listas, manipulação de status via métodos em vez de strings, e práticas recomendadas para testes (como `MailingList::fake()`). É útil para desenvolvedores que precisam implementar listas de espera com verificação de e-mail, convites e sincronização de assinantes em Laravel.

Funciona com✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/offload-project/laravel-waitlist/tree/HEAD/skills

Installed? Explore more Produtividade e Colaboração skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Contexto

offload-project/laravel-waitlist é um pacote Laravel 11/12/13 (PHP 8.3+) para gerenciar uma ou várias listas de espera. Ele fornece:

  • Um modelo Eloquent Waitlist (uma lista de espera nomeada com um slug) e um modelo WaitlistEntry (uma pessoa esperando em uma lista).
  • Um WaitlistService (resolvido através da facade Waitlist) com métodos for(), create(), add(), invite(), reject(), sendVerification(), verify() e helpers de consulta/contagem.
  • Fluxo opcional de verificação de e-mail com uma rota publicada /waitlist/verify/{token}.
  • Ponte opcional para offload-project/laravel-invite-only: chamar Waitlist::invite() cria uma Invitation real (token, expiração, eventos) e persiste a FK em WaitlistEntry::$invitation_id.
  • Duas notificações: WaitlistInvited (opt-in via auto_send_invitation) e VerifyWaitlistEmail.
  • Eventos de ciclo de vida em OffloadProject\Waitlist\Events: WaitlistCreated, WaitlistEntryAdded, WaitlistEntryVerified, WaitlistEntryInvited, WaitlistEntryRejected, além de WaitlistEntrySubscribed, WaitlistEntryUnsubscribed e MailingListSyncFailed.
  • Uma integração de lista de e-mails (facade MailingList, MailingListManager) que sincroniza entradas para Mailchimp, Kit (ConvertKit) ou Audienceful em um job enfileirado, com drivers log e array e um hook extend() para outros.
  • Exceções tipadas: UnverifiedEntryException (convite tentado em uma entrada não verificada enquanto a verificação está ativa) e MailingListException (falhas de driver/credencial/list-id/API).

Aplique este skill ao trabalhar em uma aplicação Laravel que tenha offload-project/laravel-waitlist no composer.json, ou quando o usuário pedir ajuda com Waitlist, WaitlistEntry, as facades Waitlist ou MailingList, ou fluxos de lista de espera neste pacote.

Regras

Uso da Facade

  1. Use a facade Waitlist (OffloadProject\Waitlist\Facades\Waitlist) — não instancie WaitlistService diretamente. A facade é o ponto de entrada suportado.
  2. Para segmentar uma lista de espera específica, encadeie Waitlist::for($slugOuIdOuModel)->.... Sem for(...), as chamadas operam na lista de espera padrão (criada automaticamente no primeiro uso via getDefault()).
  3. Waitlist::for(...) altera o estado interno de um serviço singleton. Para processos de longa duração (workers de fila, Octane), chame for(...) para cada operação em vez de confiar que um contexto anterior permanece.

Listas de espera vs. entradas

  1. Crie listas de espera com Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true). O slug é o identificador canônico — é o que for(...) e find(...) esperam.
  2. Adicione entradas via Waitlist::for($slug)->add($name, $email, $metadata = []). Não chame WaitlistEntry::create([...]) diretamente quando quiser que o fluxo de verificação seja executado — add() dispara automaticamente sendVerification() quando waitlist.verification.enabled é verdadeiro.
  3. A restrição única em waitlist_entries é ['waitlist_id', 'email'], não apenas email. A mesma pessoa pode entrar em várias listas de espera.

Convidando

  1. Convide via Waitlist::invite($entryOrId, $options = []). Não chame $entry->markAsInvited() sozinho quando quiser notificações e um registro Invitation — invite() cria a Invitation do laravel-invite-only, a vincula via invitation_id e marca a entrada como convidada.
  2. $options são repassados para InviteOnly::invite(...). Chaves comuns: 'invited_by' (Model ou int — recai para auth()->user()), 'role', 'metadata', 'expires_at'. Não duplique chaves já definidas em waitlist.invitable.metadata_mapper — as $options explícitas vencem via array_merge.
  3. Se waitlist.verification.enabled for verdadeiro e waitlist.verification.require_before_invite for verdadeiro, chamar invite() em uma entrada não verificada lança UnverifiedEntryException. Capture-a explicitamente em fluxos voltados ao usuário; não a esconda debaixo de um \Throwable genérico.
  4. A notificação WaitlistInvited é opt-in (waitlist.auto_send_invitation padrão é false). A notificação de convite do laravel-invite-only é enviada independentemente. Ative auto_send_invitation apenas quando quiser um segundo e-mail com a marca da lista de espera.

Verificação

  1. Dispare a verificação através de Waitlist::sendVerification($entry) — ele gera um token novo (generateVerificationToken() sobrescreve qualquer existente) e envia VerifyWaitlistEmail. Não crie sua própria geração de token; use a do pacote para que a rota de verificação continue funcionando.
  2. Confirme tokens via Waitlist::verify($token). Retorna o WaitlistEntry em caso de sucesso, null para token desconhecido. Após a verificação, o token é limpo (uso único).
  3. Personalize a notificação de verificação via configuração waitlist.verification.notification; ela recebe o WaitlistEntry em seu construtor. Leia o token de $entry->verification_token e chame route('waitlist.verify', ['token' => $entry->verification_token]).
  4. A rota de verificação do pacote é montada sob waitlist.routes.prefix (padrão waitlist) com waitlist.routes.middleware (padrão ['web']). Para usar seu próprio controller, defina waitlist.routes.enabled => false e chame Waitlist::verify($token) de sua ação.

Status e verificações

  1. Os status das entradas são os literais de string pending, invited, rejected. Prefira $entry->isPending(), isInvited(), isRejected() em vez de comparações diretas de string.
  2. O estado de verificação reside em verified_at e verification_token. Verifique via isVerified() e isPendingVerification(); não compare timestamps brutos.
  3. Para bloqueio de UI "aguarde até verificar", use isPendingVerification() (token definido, ainda não verificado). isVerified() sozinho retorna false para entradas que nunca iniciaram verificação — esses dois estados são diferentes.

Configuração de entidade convidável (invitable)

  1. Quando o aplicativo host estiver convidando pessoas para uma entidade específica (Equipe, Organização, Projeto), configure uma vez em config/waitlist.php sob invitable:
    • invitable.model — string da classe; o pacote chama ::first() nela. Use apenas para aplicativos single-tenant.
    • invitable.resolver — closure fn(WaitlistEntry $entry) => Model|null para o caso multi-tenant. Extraia o ID do tenant de $entry->metadata ou outra coluna.
    • invitable.metadata_mapper — closure fn(WaitlistEntry $entry) => array para traduzir metadados da entrada em metadados de convite (ex.: ['role' => 'beta-tester']).
  2. Não codifique um invitable por local de chamada. Se fluxos diferentes precisarem de invitables diferentes, use o closure resolver com um discriminador em metadata.

Eventos

  1. Conecte-se ao ciclo de vida com os eventos do próprio pacote em vez de envolver a facade ou consultar a tabela. Eles vivem em OffloadProject\Waitlist\Events e cada um carrega o modelo como uma propriedade somente leitura ($event->entry, $event->waitlist).
  2. Os eventos de ciclo de vida disparam dos modelos (WaitlistEntryAdded via $dispatchesEvents na criação; os demais de markAsInvited() / markAsRejected() / markAsVerified()), então listeners ainda executam para código que ignora a facade.
  3. Use Event::fake([SpecificEvent::class]) em testes, não um Event::fake() genérico — uma simulação geral também engole WaitlistEntryAdded, o que impede que o listener da lista de e-mails seja executado.

Sincronização de lista de e-mails

  1. Ative-a com waitlist.mailing_list.enabled e escolha um driver (mailchimp, kit, audienceful, log, array). Conecte uma lista de espera a uma lista com Waitlist::for($slug)->connectMailingList($listId, $driver = null) — o list id é um audience id do Mailchimp, um form id do Kit (ou tag id quando list_type é tag), ou um publication id do Audienceful (ou nome da tag quando list_type é tag). Listas de espera sem lista própria usam o list_id configurado no driver.
  2. Não construa seu próprio listener de "inscrever ao se cadastrar". A inscrição é automática e segue a configuração de verificação: com waitlist.verification.enabled desligado, a entrada sincroniza em add(), com ele ligado, a entrada sincroniza após Waitlist::verify(). Essa ordem é intencional — um endereço não confirmado nunca deve chegar à newsletter.
  3. Para qualquer coisa além de inscrever — marcar tag no convite, remover na rejeição, mover entre listas — ouça os eventos de ciclo de vida e chame MailingList::tagEntry($entry, [...]) ou Waitlist::unsubscribeFromMailingList($entry). Não adicione esses efeitos colaterais dentro dos controllers do aplicativo host.
  4. A sincronização é executada através de jobs enfileirados (SyncEntryToMailingList, UnsubscribeEntryFromMailingList). Mantenha mailing_list.queue.enabled ligado em produção para que cadastros nunca bloqueiem na API do provedor. Preencha linhas existentes com php artisan waitlist:sync-mailing-list [slug] [--all] [--force] ou Waitlist::for($slug)->syncMailingList().
  5. Adicione um serviço que o pacote não inclui implementando OffloadProject\Waitlist\Contracts\MailingListDriver e registrando-o com MailingList::extend('name', fn (array $config) => new YourDriver(...)) de um service provider. Não fork os drivers fornecidos.
  6. Em testes use MailingList::fake(), que substitui pelo ArrayDriver em memória, executa sincronizações inline e expõe hasSubscriber(), subscribers() e tagsFor(). Recorra a Http::fake() apenas quando afirmar a requisição exata que um driver real envia.

Não fazer

  1. Não execute mudanças de ciclo de vida via chamadas diretas update() ($entry->update(['status' => 'invited'])). Use markAsInvited() / markAsRejected() / markAsVerified() para que casts, efeitos colaterais (timestamps, limpeza de token) e eventos permaneçam consistentes. Melhor ainda: dirija tudo através da facade.
  2. Não edite as migrations publicadas para adicionar colunas — escreva uma migration de acompanhamento no aplicativo host. O pacote pode adicionar colunas em versões futuras e assumirá o esquema publicado.
  3. Não crie subclasses de Waitlist ou WaitlistEntry; ambos são final. Adicione comportamento no lado do aplicativo host via listeners nos eventos do pacote, ou estendendo o serviço através de uma binding personalizada no container da sua aplicação.

Exemplos

Lista de espera única (sem configuração)

use OffloadProject\Waitlist\Facades\Waitlist;

$entry = Waitlist::add('João Silva', '[email protected]', ['source' => 'landing-page']);

Waitlist::invite($entry, [
    'invited_by' => auth()->user(),
    'expires_at' => now()->addDays(14),
]);

Múltiplas listas de espera

Waitlist::create('Programa Beta', 'beta');
Waitlist::create('Acesso VIP', 'vip');

Waitlist::for('beta')->add('Maria Souza', '[email protected]');
Waitlist::for('vip')->add('Carlos Pereira', '[email protected]');

$pendentesBeta = Waitlist::for('beta')->getPending();
$contagemVip   = Waitlist::for('vip')->count();

Fluxo de verificação

// config/waitlist.php
'verification' => [
    'enabled' => true,
    'require_before_invite' => true,
    'notification' => \OffloadProject\Waitlist\Notifications\VerifyWaitlistEmail::class,
],
use OffloadProject\Waitlist\Exceptions\UnverifiedEntryException;
use OffloadProject\Waitlist\Facades\Waitlist;

$entry = Waitlist::add('João Silva', '[email protected]');
// E-mail de verificação enviado automaticamente.

try {
    Waitlist::invite($entry);
} catch (UnverifiedEntryException) {
    return back()->withErrors(['email' => 'Por favor, verifique seu e-mail primeiro.']);
}

Configurando um modelo convidável (convites de equipe)

// config/waitlist.php
'invitable' => [
    'model'    => null,
    'resolver' => fn (\OffloadProject\Waitlist\Models\WaitlistEntry $entry) =>
        \App\Models\Team::find($entry->metadata['team_id'] ?? null),
    'metadata_mapper' => fn (\OffloadProject\Waitlist\Models\WaitlistEntry $entry) => [
        'role' => $entry->metadata['role'] ?? 'member',
    ],
],

Depois:

Waitlist::for('beta')->add('Maria', '[email protected]', [
    'team_id' => $team->id,
    'role'    => 'admin',
]);

Quando você chamar Waitlist::invite($entry) posteriormente, o convite laravel-invite-only resultante será escopo para aquela equipe com role=admin.

Notificação de verificação personalizada

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use OffloadProject\Waitlist\Models\WaitlistEntry;

class CustomVerifyWaitlistEmail extends Notification
{
    public function __construct(public WaitlistEntry $entry) {}

    public function via($notifiable): array
    {
        return ['mail'];
    }

    public function toMail($notifiable): MailMessage
    {
        $url = route('waitlist.verify', ['token' => $this->entry->verification_token]);

        return (new MailMessage)
            ->subject('Confirme seu lugar na lista de espera')
            ->greeting("Olá {$this->entry->name}!")
            ->action('Verificar E-mail', $url);
    }
}
// config/waitlist.php
'verification' => [
    'enabled' => true,
    'notification' => \App\Notifications\CustomVerifyWaitlistEmail::class,
],

Sincronizando cadastros para Mailchimp

// config/waitlist.php
'mailing_list' => [
    'enabled' => true,
    'default' => 'mailchimp',
    'drivers' => [
        'mailchimp' => [
            'key' => env('MAILCHIMP_API_KEY'),   // o sufixo carrega o data centre, ex.: -us14
            'list_id' => env('MAILCHIMP_LIST_ID'),
        ],
    ],
],
// Um público por lista de espera (opcional — caso contrário, o list_id da configuração é usado).
Waitlist::for('beta')->connectMailingList('a1b2c3d4e5');

// Inscrito automaticamente, na adição ou na verificação dependendo da configuração.
Waitlist::for('beta')->add('Maria Souza', '[email protected]');

Reagindo ao ciclo de vida

use Illuminate\Support\Facades\Event;
use OffloadProject\Waitlist\Events\WaitlistEntryInvited;
use OffloadProject\Waitlist\Events\WaitlistEntryRejected;
use OffloadProject\Waitlist\Facades\MailingList;
use OffloadProject\Waitlist\Facades\Waitlist;

Event::listen(fn (WaitlistEntryInvited $event) => MailingList::tagEntry($event->entry, ['invited']));
Event::listen(fn (WaitlistEntryRejected $event) => Waitlist::unsubscribeFromMailingList($event->entry));

Testando um fluxo de lista de e-mails

use OffloadProject\Waitlist\Facades\MailingList;
use OffloadProject\Waitlist\Facades\Waitlist;

MailingList::fake();

$entry = Waitlist::for('beta')->add('Teste', '[email protected]');

// Afirme que o assinante foi inscrito na lista.
MailingList::assertSubscriberAdded('[email protected]');

// Ou inspecione a lista.
MailingList::hasSubscriber('[email protected]'); // true
// Testando convites rejeitados
MailingList::fake();
$entry = Waitlist::for('beta')->add('Teste', '[email protected]');
Waitlist::reject($entry);

// O assinante foi removido da lista de e-mails.
MailingList::assertSubscriberRemoved('[email protected]');

Para testar eventos específicos, use:

use Illuminate\Support\Facades\Event;
use OffloadProject\Waitlist\Events\WaitlistEntryAdded;

Event::fake([WaitlistEntryAdded::class]);

// ... execute sua ação

Event::assertDispatched(WaitlistEntryAdded::class);

Habilidades Relacionadas