Plugin de agendamento 24 horas para manutenção de tubulação: automação total

Desenvolva um plugin WordPress para agendamento 24h de manutenção de tubulação com cron jobs, API REST e painel administrativo. Guia técnico completo.

Imagine um condomínio de 200 unidades na zona norte de São Paulo onde a coluna principal de esgoto entope às 3h da madrugada. O síndico acessa o site da administradora, seleciona “Emergência 24h”, escolhe o primeiro horário disponível — 3h30 — e confirma. Às 3h25 a equipe chega com hidrojateamento. Nenhum humano mediou o agendamento; o plugin verificou plantão, técnico mais próximo, estoque de equipamento e confirmou em segundos. Esse cenário real exige arquitetura que não depende de WP-Cron nativo, não trava em concorrência e expõe API limpa para integrações móveis e WhatsApp.

A complexidade não está no formulário, mas na orquestração: regras de recorrência (“todo dia útil das 7h às 19h, exceto feriados municipais”), exceções (“técnico João de férias 15 a 20/out”), conflitos de recurso (um caminhão de hidrojateamento para três equipes) e SLA de resposta (< 5 min para emergências). Resolver isso no WordPress exige sair da zona de conforto de posts e metas para tabelas otimizadas, filas assíncronas e versionamento de schema.

Arquitetura de dados: CPTs vs tabelas customizadas

Custom Post Types funcionam bem para entidades ricas em conteúdo (Plantões, Técnicos com biografia, Equipamentos com fichas técnicas). Para slots de horário — milhões de linhas, consultas por range temporal, locks de concorrência — CPTs degradam performance. Crie tabela wp_pipeline_slots com colunas:

CREATE TABLE wp_pipeline_slots (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  technician_id BIGINT UNSIGNED NOT NULL,
  service_type_id BIGINT UNSIGNED NOT NULL,
  start_ts DATETIME NOT NULL,
  end_ts DATETIME NOT NULL,
  status ENUM('free','held','booked','blocked') NOT NULL DEFAULT 'free',
  booking_id BIGINT UNSIGNED NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  INDEX idx_tech_time (technician_id, start_ts),
  INDEX idx_status_time (status, start_ts),
  INDEX idx_booking (booking_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Use $wpdb para CRUD direto nessa tabela. Registre CPT pipeline_technician com metabox para skills (hidrojateamento, caça-vazamento, inspeção por vídeo) e zona de atendimento (geojson polígono). CPT pipeline_shift armazena regras RRULE no meta _rrule e exceções em _exdates (array de datas ISO). O plugin área do cliente para histórico de manutenções em Moema demonstra padrão semelhante de separação entre entidades de conteúdo e dados transacionais de alta frequência.

WP-Cron real: systemd timer no servidor

O WP-Cron nativo só roda quando há requisição HTTP. Em madrugadas sem tráfego, agendamentos de notificação (“técnico a caminho”) e limpeza de slots expirados não disparam. Solução: desative DISABLE_WP_CRON no wp-config.php e crie /etc/systemd/system/wp-cron-pipeline.service:

[Unit]
Description=WP-Cron for Pipeline Maintenance Plugin
After=network.target

[Service]
Type=oneshot
User=www-data
ExecStart=/usr/bin/curl -s -o /dev/null "https://seusite.com/wp-cron.php?doing_wp_cron=1"
WorkingDirectory=/var/www/html

E o timer /etc/systemd/system/wp-cron-pipeline.timer:

[Unit]
Description=Run WP-Cron every minute

[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
AccuracySec=1s

[Install]
WantedBy=timers.target

Ative com systemctl enable --now wp-cron-pipeline.timer. No plugin, agende tarefas recorrentes com wp_schedule_event(time(), 'minute', 'pipeline_cleanup_expired_holds'). O hook pipeline_cleanup_expired_holds libera slots em status ‘held’ há mais de 30 minutos (abandono de checkout).

Endpoints REST com capacidades granulares

Registre namespace pipeline/v1 no rest_api_init:

register_rest_route('pipeline/v1', '/slots', [
  'methods' => 'GET',
  'callback' => [$this, 'get_available_slots'],
  'permission_callback' => '__return_true', // público para leitura
  'args' => [
    'technician_id' => ['type' => 'integer', 'validate_callback' => 'rest_validate_request_arg'],
    'service_type' => ['type' => 'string', 'enum' => ['hydrojetting','leak_detection','video_inspection']],
    'start' => ['type' => 'string', 'format' => 'date-time', 'required' => true],
    'end' => ['type' => 'string', 'format' => 'date-time', 'required' => true],
    'lat' => ['type' => 'number'],
    'lng' => ['type' => 'number'],
    'radius_km' => ['type' => 'integer', 'default' => 25],
  ],
]);

register_rest_route('pipeline/v1', '/bookings', [
  'methods' => 'POST',
  'callback' => [$this, 'create_booking'],
  'permission_callback' => function() { return current_user_can('pipeline_book_emergency'); },
  'args' => [
    'slot_id' => ['type' => 'integer', 'required' => true],
    'client_notes' => ['type' => 'string', 'max_length' => 500],
    'contact_phone' => ['type' => 'string', 'pattern' => '^\+55\d{10,11}$', 'required' => true],
  ],
]);

Capacidade customizada pipeline_book_emergency atribuída a role pipeline_customer (criada no register_activation_hook). Endpoints administrativos (/technicians, /shifts, /reports) exigem manage_pipeline_maintenance. Use rest_pre_dispatch para rate limiting: 30 req/min por IP no GET /slots, 5 req/min no POST /bookings.

Locking otimista com transients no checkout

Quando o usuário clica “Confirmar agendamento” no front-end:

  1. Front envia slot_id para endpoint POST /api/pipeline/v1/hold-slot
  2. Backend verifica se status = 'free'
  3. Tenta set_transient("pipeline_hold_{$slot_id}", get_current_user_id(), 30) — retorna false se já existe
  4. Se sucesso, atualiza slot para status = 'held', booking_id = 0 (temporário)
  5. Retorna {hold_token: "pipeline_hold_{$slot_id}", expires_in: 30}
  6. Front tem 30s para enviar POST /bookings com hold_token
  7. Backend valida transient, cria pipeline_booking (CPT), atualiza slot para booked com booking_id real, deleta transient
  8. Se falha ou expira, cron job pipeline_cleanup_expired_holds volta slot para free

Esse padrão evita race condition sem transações de banco longas. O artigo agência WordPress gerencia tempo para reparo de direção hidráulica em aborda desafio similar de gestão de slots de oficina com concorrência.

Fila de notificações assíncrona com Action Scheduler

Após confirmar agendamento, não chame API do WhatsApp nem dispare e-mail no mesmo request. Agende actions:

action_scheduler_schedule_action(
  'pipeline_send_booking_confirmation',
  time(), // agora
  [
    'booking_id' => $booking_id,
    'channels' => ['whatsapp', 'email', 'push'],
  ],
  'pipeline_notifications'
);

Crie queue pipeline_notifications no Action Scheduler (tabela wp_actionscheduler_actions). O handler pipeline_send_booking_confirmation itera canais:

  • WhatsApp: POST para webhook Zenvia/Twilio com template aprovado (HSM)
  • E-mail: wp_mail com template HTML + texto
  • Push: WebPush library enviando para subscriptions salvas no user meta _webpush_subscriptions

Falhas retentam com backoff exponencial (Action Scheduler nativo). Logs estruturados via WC_Logger (compatível PSR-3) gravam em wp-content/logs/pipeline-booking-YYYY-MM-DD.log com contexto booking_id, channel, attempt, response_code.

Gestão visual de recorrências no admin

No CPT pipeline_shift, adicione metabox Gutenberg (register_block_type com serverSideRender) que renderiza componente React com:

  • Date picker para dtstart e until
  • Select de frequência (DAILY, WEEKLY, MONTHLY) + byday (MO,TU,WE,TH,FR)
  • Input para bymonthday (ex: -1 = último dia do mês)
  • Área de exceções: lista de datas com botão “Adicionar feriado”
  • Preview calculando próximas 10 ocorrências via rrule-js

Ao salvar, serializa RRULE no meta _rrule (ex: FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;DTSTART=20240101T070000Z) e _exdates como array ['20240421','20241120']. Cron job diário pipeline_generate_slots_from_shifts lê shifts ativos, expande RRULE para próximos 60 dias, insere slots na tabela wp_pipeline_slots pulando exdates e feriados nacionais (API https://brasilapi.com.br/api/feriados/v1/{ano}).

Migração de schema versionada

No arquivo principal do plugin:

define('PIPELINE_SCHEMA_VERSION', '3.2.0');

register_activation_hook(__FILE__, function() {
  $current = get_option('pipeline_schema_version', '0.0.0');
  if (version_compare($current, PIPELINE_SCHEMA_VERSION, '<')) {
    pipeline_migrate_schema($current);
    update_option('pipeline_schema_version', PIPELINE_SCHEMA_VERSION);
  }
});

Função pipeline_migrate_schema($from_version) executa steps sequenciais:

function pipeline_migrate_schema($from) {
  $steps = [
    '1.0.0' => 'pipeline_migrate_100_create_slots_table',
    '2.0.0' => 'pipeline_migrate_200_add_geojson_to_technicians',
    '3.0.0' => 'pipeline_migrate_300_split_shifts_recurrence',
    '3.2.0' => 'pipeline_migrate_320_add_booking_source_field',
  ];
  foreach ($steps as $version => $callback) {
    if (version_compare($from, $version, '<') && function_exists($callback)) {
      global $wpdb;
      $wpdb->query('CREATE TABLE IF NOT EXISTS wp_pipeline_migration_backup AS SELECT * FROM wp_pipeline_slots WHERE 1=0');
      $callback();
      update_option('pipeline_schema_version', $version);
    }
  }
}

Cada callback é idempotente (CREATE TABLE IF NOT EXISTS, ALTER TABLE ADD COLUMN IF NOT EXISTS). Backup automático antes de cada step permite rollback manual se necessário.

Testes de carga e observabilidade

Antes de ir para produção, rode k6 com cenário realista:

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 50 },
    { duration: '1m', target: 200 },
    { duration: '30s', target: 0 },
  ],
  thresholds: {
    'http_req_duration': ['p(95)<500'],
    'http_req_failed': ['rate<0.01'],
  },
};

export default function() {
  const params = { headers: { 'Accept': 'application/json' } };
  const res = http.get('https://staging.seusite.com/wp-json/pipeline/v1/slots?service_type=hydrojetting&start=2024-12-01T00:00:00Z&end=2024-12-02T00:00:00Z', params);
  check(res, { 'status 200': (r) => r.status === 200, 'slots array': (r) => Array.isArray(r.json()) });
  sleep(0.1);
}

Monitore com Query Monitor (tempo de query SQL, hooks lentos) e New Relic/DataDog (latência p95, taxa de erro). Alerte se pipeline_cleanup_expired_holds processar > 1000 slots por execução (indica problema no fluxo de checkout).

A manutenção de tubulação SP exige sistemas que não dormem. Com tabela otimizada para slots, cron real no systemd, locking via transients, fila assíncrona para notificações e migrações versionadas, seu plugin atende emergências 3h da manhã com a mesma confiabilidade do meio-dia. O próximo passo é instrumentar métricas de negócio — tempo médio entre chamado e chegada, taxa de primeira visita resolvida, ociosidade de técnicos — para transformar o plugin em ferramenta de gestão, não apenas agendamento.

Para integração completa com equipes de campo e relatórios técnicos, consulte o painel de manutenção de tubulação SP que centraliza chamados, rotas e evidências fotográficas em tempo real.

Checklist prático

0 de 9 concluídos

Perguntas frequentes

Como garantir que o agendamento funcione 24 horas sem depender de visitas ao site?

Configure um cron job real no servidor (systemd timer ou crontab) que dispare o endpoint WP-Cron a cada minuto. O WordPress sozinho só executa tarefas agendadas quando há tráfego, o que falha em madrugadas sem acessos.

Qual a melhor forma de armazenar horários de plantão e feriados no plugin?

Crie Custom Post Types para 'Plantões' e 'Regras de Exceção' com campos ACF ou metaboxes nativos. Use taxonomias para tipos de serviço (hidrojateamento, caça-vazamento) e associe regras de recorrência via RRULE (iCal) para flexibilidade total.

Como evitar conflitos de horário quando dois clientes agendam o mesmo técnico?

Implemente locking otimista com transients no momento da reserva: verifique disponibilidade, crie transient de 30s com o slot tentado e só confirme se o transient ainda for seu. Libere o slot se o pagamento falhar ou o usuário abandonar o fluxo.

O plugin deve expor endpoints REST públicos ou apenas administrativos?

Exponha endpoints públicos para leitura de disponibilidade (GET /api/v1/slots) e criação de agendamentos (POST /api/v1/bookings) com nonces e rate limiting. Endpoints de gestão de técnicos e relatórios ficam restritos a roles com capability 'manage_pipeline_maintenance'.

Como integrar notificações push e WhatsApp sem travar o fluxo de agendamento?

Despache notificações via Action Scheduler ou fila RabbitMQ/Redis após o agendamento ser confirmado no banco. Use webhooks assíncronos para provedores de WhatsApp (Zenvia, Twilio) e Service Workers para push no navegador, garantindo resposta imediata ao usuário.

Qual estratégia de migração de dados ao atualizar a estrutura de horários?

Registre a versão do schema no option 'pipeline_schema_version'. No hook de ativação/upgrade, compare versões e execute rotinas de migração sequenciais (ex: converter meta fields serializados para CPTs). Mantenha rollback automático via backup de tabela antes de cada step.