feat(api): implement javascript-first architecture for cache compatibility
- Add REST endpoint GET /roi-theme/v1/adsense-placement/visibility - Add Domain layer: UserContext, VisibilityDecision, AdsenseSettings VOs - Add Application layer: CheckAdsenseVisibilityUseCase - Add Infrastructure: AdsenseVisibilityChecker, Controller, Enqueuer - Add JavaScript controller with localStorage caching - Add test plan for production validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Application\UseCases;
|
||||
|
||||
use ROITheme\Public\AdsensePlacement\Domain\Contracts\AdsenseVisibilityCheckerInterface;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\UserContext;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\VisibilityDecision;
|
||||
|
||||
/**
|
||||
* Use Case para verificar visibilidad de anuncios AdSense.
|
||||
*
|
||||
* Application Layer - Orquesta el checker de domain.
|
||||
* No contiene logica de negocio, solo coordina.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Application\UseCases
|
||||
*/
|
||||
final class CheckAdsenseVisibilityUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private AdsenseVisibilityCheckerInterface $visibilityChecker
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta la verificacion de visibilidad.
|
||||
*
|
||||
* @param int $postId ID del post (0 para paginas de archivo/home)
|
||||
* @param UserContext $userContext Contexto del usuario
|
||||
* @return VisibilityDecision Decision de visibilidad
|
||||
*/
|
||||
public function execute(int $postId, UserContext $userContext): VisibilityDecision
|
||||
{
|
||||
return $this->visibilityChecker->check($postId, $userContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\Contracts;
|
||||
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\UserContext;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\VisibilityDecision;
|
||||
|
||||
/**
|
||||
* Interface para el servicio que evalua visibilidad de anuncios.
|
||||
*
|
||||
* Domain Layer - Define el contrato sin dependencias de infraestructura.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Domain\Contracts
|
||||
*/
|
||||
interface AdsenseVisibilityCheckerInterface
|
||||
{
|
||||
/**
|
||||
* Evalua si los anuncios deben mostrarse para el contexto dado.
|
||||
*
|
||||
* @param int $postId ID del post (0 para paginas de archivo/home)
|
||||
* @param UserContext $userContext Contexto del usuario
|
||||
* @return VisibilityDecision Decision con razones y cache
|
||||
*/
|
||||
public function check(int $postId, UserContext $userContext): VisibilityDecision;
|
||||
}
|
||||
163
Public/AdsensePlacement/Domain/ValueObjects/AdsenseSettings.php
Normal file
163
Public/AdsensePlacement/Domain/ValueObjects/AdsenseSettings.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object que encapsula la configuracion de AdSense relevante para visibilidad.
|
||||
*
|
||||
* Inmutable despues de construccion. Sin dependencias de WordPress.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Domain\ValueObjects
|
||||
*/
|
||||
final class AdsenseSettings
|
||||
{
|
||||
/**
|
||||
* @param bool $isEnabled Si AdSense esta activo globalmente
|
||||
* @param bool $showOnDesktop Si se muestra en desktop
|
||||
* @param bool $showOnMobile Si se muestra en mobile
|
||||
* @param bool $hideForLoggedIn Si se oculta para usuarios logueados
|
||||
* @param bool $javascriptFirstMode Si el modo JS-first esta activo
|
||||
* @param array<int> $excludedCategoryIds IDs de categorias excluidas
|
||||
* @param array<int> $excludedPostIds IDs de posts excluidos
|
||||
* @param array<string> $excludedPostTypes Post types excluidos
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $isEnabled,
|
||||
private bool $showOnDesktop,
|
||||
private bool $showOnMobile,
|
||||
private bool $hideForLoggedIn,
|
||||
private bool $javascriptFirstMode,
|
||||
private array $excludedCategoryIds = [],
|
||||
private array $excludedPostIds = [],
|
||||
private array $excludedPostTypes = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->isEnabled;
|
||||
}
|
||||
|
||||
public function showOnDesktop(): bool
|
||||
{
|
||||
return $this->showOnDesktop;
|
||||
}
|
||||
|
||||
public function showOnMobile(): bool
|
||||
{
|
||||
return $this->showOnMobile;
|
||||
}
|
||||
|
||||
public function hideForLoggedIn(): bool
|
||||
{
|
||||
return $this->hideForLoggedIn;
|
||||
}
|
||||
|
||||
public function isJavascriptFirstMode(): bool
|
||||
{
|
||||
return $this->javascriptFirstMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getExcludedCategoryIds(): array
|
||||
{
|
||||
return $this->excludedCategoryIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getExcludedPostIds(): array
|
||||
{
|
||||
return $this->excludedPostIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getExcludedPostTypes(): array
|
||||
{
|
||||
return $this->excludedPostTypes;
|
||||
}
|
||||
|
||||
public function isPostExcluded(int $postId): bool
|
||||
{
|
||||
return in_array($postId, $this->excludedPostIds, true);
|
||||
}
|
||||
|
||||
public function isPostTypeExcluded(string $postType): bool
|
||||
{
|
||||
return in_array($postType, $this->excludedPostTypes, true);
|
||||
}
|
||||
|
||||
public function isCategoryExcluded(int $categoryId): bool
|
||||
{
|
||||
return in_array($categoryId, $this->excludedCategoryIds, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea instancia desde array de configuracion de BD.
|
||||
*
|
||||
* @param array<string, array<string, mixed>> $settings Configuracion agrupada
|
||||
*/
|
||||
public static function fromArray(array $settings): self
|
||||
{
|
||||
$visibility = $settings['visibility'] ?? [];
|
||||
$behavior = $settings['behavior'] ?? [];
|
||||
$forms = $settings['forms'] ?? [];
|
||||
|
||||
return new self(
|
||||
isEnabled: (bool) ($visibility['is_enabled'] ?? false),
|
||||
showOnDesktop: (bool) ($visibility['show_on_desktop'] ?? true),
|
||||
showOnMobile: (bool) ($visibility['show_on_mobile'] ?? true),
|
||||
hideForLoggedIn: (bool) ($visibility['hide_for_logged_in'] ?? false),
|
||||
javascriptFirstMode: (bool) ($behavior['javascript_first_mode'] ?? false),
|
||||
excludedCategoryIds: self::parseIds($forms['exclude_categories'] ?? ''),
|
||||
excludedPostIds: self::parseIds($forms['exclude_post_ids'] ?? ''),
|
||||
excludedPostTypes: self::parsePostTypes($forms['exclude_post_types'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsea string de IDs separados por coma a array de enteros.
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
private static function parseIds(string $value): array
|
||||
{
|
||||
if (empty($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
array_map(
|
||||
static fn(string $id): int => (int) trim($id),
|
||||
explode(',', $value)
|
||||
),
|
||||
static fn(int $id): bool => $id > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsea string de post types separados por coma.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private static function parsePostTypes(string $value): array
|
||||
{
|
||||
if (empty($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
array_map(
|
||||
static fn(string $type): string => trim($type),
|
||||
explode(',', $value)
|
||||
),
|
||||
static fn(string $type): bool => $type !== ''
|
||||
);
|
||||
}
|
||||
}
|
||||
82
Public/AdsensePlacement/Domain/ValueObjects/UserContext.php
Normal file
82
Public/AdsensePlacement/Domain/ValueObjects/UserContext.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object que encapsula el contexto del usuario para decisiones de visibilidad.
|
||||
*
|
||||
* Inmutable despues de construccion. Sin dependencias de WordPress.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Domain\ValueObjects
|
||||
*/
|
||||
final class UserContext
|
||||
{
|
||||
/**
|
||||
* @param bool $isLoggedIn Si el usuario tiene sesion activa
|
||||
* @param bool $isMobile Si el dispositivo es movil (viewport < 992px)
|
||||
* @param array<int> $userRoles IDs de roles del usuario
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $isLoggedIn,
|
||||
private bool $isMobile,
|
||||
private array $userRoles = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function isLoggedIn(): bool
|
||||
{
|
||||
return $this->isLoggedIn;
|
||||
}
|
||||
|
||||
public function isMobile(): bool
|
||||
{
|
||||
return $this->isMobile;
|
||||
}
|
||||
|
||||
public function isDesktop(): bool
|
||||
{
|
||||
return !$this->isMobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getUserRoles(): array
|
||||
{
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
public function hasRole(int $roleId): bool
|
||||
{
|
||||
return in_array($roleId, $this->userRoles, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea instancia desde array (para deserializacion).
|
||||
*
|
||||
* @param array{is_logged_in: bool, is_mobile: bool, user_roles?: array<int>} $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
isLoggedIn: $data['is_logged_in'] ?? false,
|
||||
isMobile: $data['is_mobile'] ?? false,
|
||||
userRoles: $data['user_roles'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializa a array para respuesta JSON.
|
||||
*
|
||||
* @return array{is_logged_in: bool, is_mobile: bool, user_roles: array<int>}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'is_logged_in' => $this->isLoggedIn,
|
||||
'is_mobile' => $this->isMobile,
|
||||
'user_roles' => $this->userRoles,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object que representa la decision de mostrar o no anuncios.
|
||||
*
|
||||
* Inmutable despues de construccion. Sin dependencias de WordPress.
|
||||
* El timestamp se inyecta en toArray() para mantener Domain puro.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Domain\ValueObjects
|
||||
*/
|
||||
final class VisibilityDecision
|
||||
{
|
||||
private const DEFAULT_CACHE_SECONDS = 300; // 5 minutos
|
||||
|
||||
/**
|
||||
* @param bool $showAds Si se deben mostrar los anuncios
|
||||
* @param array<string> $reasons Razones de la decision (para debugging)
|
||||
* @param int $cacheSeconds Tiempo de cache en segundos
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $showAds,
|
||||
private array $reasons = [],
|
||||
private int $cacheSeconds = self::DEFAULT_CACHE_SECONDS
|
||||
) {
|
||||
}
|
||||
|
||||
public function shouldShowAds(): bool
|
||||
{
|
||||
return $this->showAds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getReasons(): array
|
||||
{
|
||||
return $this->reasons;
|
||||
}
|
||||
|
||||
public function getCacheSeconds(): int
|
||||
{
|
||||
return $this->cacheSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: Crea decision positiva (mostrar ads).
|
||||
*/
|
||||
public static function show(int $cacheSeconds = self::DEFAULT_CACHE_SECONDS): self
|
||||
{
|
||||
return new self(
|
||||
showAds: true,
|
||||
reasons: ['all_conditions_passed'],
|
||||
cacheSeconds: $cacheSeconds
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: Crea decision negativa (ocultar ads).
|
||||
*
|
||||
* @param array<string> $reasons
|
||||
*/
|
||||
public static function hide(array $reasons, int $cacheSeconds = self::DEFAULT_CACHE_SECONDS): self
|
||||
{
|
||||
return new self(
|
||||
showAds: false,
|
||||
reasons: $reasons,
|
||||
cacheSeconds: $cacheSeconds
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializa a array para respuesta JSON.
|
||||
*
|
||||
* El timestamp se inyecta aqui (no en constructor) para mantener Domain puro.
|
||||
*
|
||||
* @param int $timestamp Unix timestamp actual
|
||||
* @return array{show_ads: bool, reasons: array<string>, cache_seconds: int, timestamp: int}
|
||||
*/
|
||||
public function toArray(int $timestamp): array
|
||||
{
|
||||
return [
|
||||
'show_ads' => $this->showAds,
|
||||
'reasons' => $this->reasons,
|
||||
'cache_seconds' => $this->cacheSeconds,
|
||||
'timestamp' => $timestamp,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Public\AdsensePlacement\Application\UseCases\CheckAdsenseVisibilityUseCase;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\UserContext;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
|
||||
/**
|
||||
* REST Controller para el endpoint de visibilidad de AdSense.
|
||||
*
|
||||
* Infrastructure Layer - Maneja HTTP y traduce a/desde Domain.
|
||||
*
|
||||
* Endpoint: GET /wp-json/roi-theme/v1/adsense-placement/visibility
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Infrastructure\Api\WordPress
|
||||
*/
|
||||
final class AdsenseVisibilityController
|
||||
{
|
||||
private const NAMESPACE = 'roi-theme/v1';
|
||||
private const ROUTE = '/adsense-placement/visibility';
|
||||
private const NONCE_ACTION = 'roi_adsense_visibility';
|
||||
|
||||
public function __construct(
|
||||
private CheckAdsenseVisibilityUseCase $useCase
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra la ruta REST del endpoint.
|
||||
*/
|
||||
public function registerRoutes(): void
|
||||
{
|
||||
register_rest_route(self::NAMESPACE, self::ROUTE, [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'handleRequest'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'post_id' => [
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'sanitize_callback' => 'absint',
|
||||
// IMPORTANTE: postId=0 es valido (paginas de archivo, home, etc.)
|
||||
'validate_callback' => static fn($value): bool => $value >= 0,
|
||||
],
|
||||
'nonce' => [
|
||||
'required' => false,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja la peticion REST.
|
||||
*/
|
||||
public function handleRequest(WP_REST_Request $request): WP_REST_Response
|
||||
{
|
||||
$this->sendNoCacheHeaders();
|
||||
|
||||
// Validar nonce si se proporciona
|
||||
$nonce = $request->get_param('nonce');
|
||||
if ($nonce !== null && !wp_verify_nonce($nonce, self::NONCE_ACTION)) {
|
||||
return new WP_REST_Response([
|
||||
'show_ads' => false,
|
||||
'reasons' => ['invalid_nonce'],
|
||||
'cache_seconds' => 0,
|
||||
'timestamp' => time(),
|
||||
], 403);
|
||||
}
|
||||
|
||||
$postId = (int) $request->get_param('post_id');
|
||||
$userContext = $this->buildUserContext();
|
||||
|
||||
$decision = $this->useCase->execute($postId, $userContext);
|
||||
|
||||
// El timestamp se inyecta aqui (Infrastructure) para mantener Domain puro
|
||||
return new WP_REST_Response($decision->toArray(time()), 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye UserContext desde el estado actual de WordPress.
|
||||
*/
|
||||
private function buildUserContext(): UserContext
|
||||
{
|
||||
$isLoggedIn = is_user_logged_in();
|
||||
$userRoles = [];
|
||||
|
||||
if ($isLoggedIn) {
|
||||
$user = wp_get_current_user();
|
||||
$userRoles = array_map(
|
||||
static fn(string $role): int => self::roleToId($role),
|
||||
$user->roles
|
||||
);
|
||||
}
|
||||
|
||||
// isMobile se determina por el cliente, no el servidor
|
||||
// El cliente enviara esta info, pero por defecto asumimos false
|
||||
$isMobile = false;
|
||||
|
||||
return new UserContext(
|
||||
isLoggedIn: $isLoggedIn,
|
||||
isMobile: $isMobile,
|
||||
userRoles: $userRoles
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte nombre de rol a ID numerico para consistencia.
|
||||
*/
|
||||
private static function roleToId(string $role): int
|
||||
{
|
||||
$roleMap = [
|
||||
'administrator' => 1,
|
||||
'editor' => 2,
|
||||
'author' => 3,
|
||||
'contributor' => 4,
|
||||
'subscriber' => 5,
|
||||
];
|
||||
|
||||
return $roleMap[$role] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envia headers para prevenir cache en proxies/CDNs.
|
||||
*/
|
||||
private function sendNoCacheHeaders(): void
|
||||
{
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
|
||||
header('Vary: Cookie');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la accion del nonce para generacion en frontend.
|
||||
*/
|
||||
public static function getNonceAction(): string
|
||||
{
|
||||
return self::NONCE_ACTION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Providers;
|
||||
|
||||
use ROITheme\Shared\Infrastructure\Di\DIContainer;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\Contracts\AdsenseVisibilityCheckerInterface;
|
||||
use ROITheme\Public\AdsensePlacement\Infrastructure\Services\AdsenseVisibilityChecker;
|
||||
use ROITheme\Public\AdsensePlacement\Application\UseCases\CheckAdsenseVisibilityUseCase;
|
||||
use ROITheme\Public\AdsensePlacement\Infrastructure\Api\WordPress\AdsenseVisibilityController;
|
||||
use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsenseAssetsEnqueuer;
|
||||
|
||||
/**
|
||||
* Service Provider para el sistema JavaScript-First de AdSense.
|
||||
*
|
||||
* Registra todas las dependencias y hooks necesarios.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Infrastructure\Providers
|
||||
*/
|
||||
final class AdsenseJavascriptFirstServiceProvider
|
||||
{
|
||||
private ?AdsenseVisibilityCheckerInterface $visibilityChecker = null;
|
||||
private ?CheckAdsenseVisibilityUseCase $useCase = null;
|
||||
private ?AdsenseVisibilityController $controller = null;
|
||||
private ?AdsenseAssetsEnqueuer $enqueuer = null;
|
||||
|
||||
public function __construct(
|
||||
private DIContainer $container
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra los servicios en el contenedor.
|
||||
*
|
||||
* Llamar en after_setup_theme.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Los servicios se crean lazy en boot()
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa los hooks de WordPress.
|
||||
*
|
||||
* Llamar en init.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Registrar REST API endpoint
|
||||
add_action('rest_api_init', function(): void {
|
||||
$this->getController()->registerRoutes();
|
||||
});
|
||||
|
||||
// Registrar enqueue de assets
|
||||
$this->getEnqueuer()->register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el checker de visibilidad (lazy initialization).
|
||||
*/
|
||||
public function getVisibilityChecker(): AdsenseVisibilityCheckerInterface
|
||||
{
|
||||
if ($this->visibilityChecker === null) {
|
||||
$this->visibilityChecker = new AdsenseVisibilityChecker(
|
||||
$this->container->getComponentSettingsRepository()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->visibilityChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el use case (lazy initialization).
|
||||
*/
|
||||
public function getUseCase(): CheckAdsenseVisibilityUseCase
|
||||
{
|
||||
if ($this->useCase === null) {
|
||||
$this->useCase = new CheckAdsenseVisibilityUseCase(
|
||||
$this->getVisibilityChecker()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->useCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el controller REST (lazy initialization).
|
||||
*/
|
||||
public function getController(): AdsenseVisibilityController
|
||||
{
|
||||
if ($this->controller === null) {
|
||||
$this->controller = new AdsenseVisibilityController(
|
||||
$this->getUseCase()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el enqueuer de assets (lazy initialization).
|
||||
*/
|
||||
public function getEnqueuer(): AdsenseAssetsEnqueuer
|
||||
{
|
||||
if ($this->enqueuer === null) {
|
||||
$this->enqueuer = new AdsenseAssetsEnqueuer(
|
||||
$this->container->getComponentSettingsRepository()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->enqueuer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Public\AdsensePlacement\Domain\Contracts\AdsenseVisibilityCheckerInterface;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\UserContext;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\VisibilityDecision;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\AdsenseSettings;
|
||||
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Implementacion del checker de visibilidad de AdSense.
|
||||
*
|
||||
* Infrastructure Layer - Accede a BD via repository.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Infrastructure\Services
|
||||
*/
|
||||
final class AdsenseVisibilityChecker implements AdsenseVisibilityCheckerInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'adsense-placement';
|
||||
private const CACHE_SECONDS_SHOW = 300; // 5 min cuando se muestran ads
|
||||
private const CACHE_SECONDS_HIDE = 600; // 10 min cuando se ocultan (menos volatil)
|
||||
|
||||
public function __construct(
|
||||
private ComponentSettingsRepositoryInterface $settingsRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function check(int $postId, UserContext $userContext): VisibilityDecision
|
||||
{
|
||||
// Cargar configuracion desde BD
|
||||
$rawSettings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
|
||||
$settings = AdsenseSettings::fromArray($rawSettings);
|
||||
|
||||
// Evaluar reglas de visibilidad
|
||||
$reasons = [];
|
||||
|
||||
// 1. AdSense desactivado globalmente
|
||||
if (!$settings->isEnabled()) {
|
||||
return VisibilityDecision::hide(['adsense_disabled'], self::CACHE_SECONDS_HIDE);
|
||||
}
|
||||
|
||||
// 2. JavaScript-First Mode desactivado (usar PHP legacy)
|
||||
if (!$settings->isJavascriptFirstMode()) {
|
||||
return VisibilityDecision::hide(['javascript_first_disabled'], self::CACHE_SECONDS_HIDE);
|
||||
}
|
||||
|
||||
// 3. Ocultar para usuarios logueados
|
||||
if ($settings->hideForLoggedIn() && $userContext->isLoggedIn()) {
|
||||
$reasons[] = 'user_logged_in';
|
||||
}
|
||||
|
||||
// 4. Verificar dispositivo
|
||||
if ($userContext->isMobile() && !$settings->showOnMobile()) {
|
||||
$reasons[] = 'mobile_disabled';
|
||||
}
|
||||
|
||||
if ($userContext->isDesktop() && !$settings->showOnDesktop()) {
|
||||
$reasons[] = 'desktop_disabled';
|
||||
}
|
||||
|
||||
// 5. Verificar exclusiones de post (solo si postId > 0)
|
||||
if ($postId > 0) {
|
||||
if ($settings->isPostExcluded($postId)) {
|
||||
$reasons[] = 'post_excluded';
|
||||
}
|
||||
|
||||
// Verificar categorias del post
|
||||
$postCategories = $this->getPostCategoryIds($postId);
|
||||
foreach ($postCategories as $catId) {
|
||||
if ($settings->isCategoryExcluded($catId)) {
|
||||
$reasons[] = 'category_excluded';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar post type
|
||||
$postType = $this->getPostType($postId);
|
||||
if ($postType !== '' && $settings->isPostTypeExcluded($postType)) {
|
||||
$reasons[] = 'post_type_excluded';
|
||||
}
|
||||
}
|
||||
|
||||
// Decision final
|
||||
if (count($reasons) > 0) {
|
||||
return VisibilityDecision::hide($reasons, self::CACHE_SECONDS_HIDE);
|
||||
}
|
||||
|
||||
return VisibilityDecision::show(self::CACHE_SECONDS_SHOW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene IDs de categorias de un post.
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
private function getPostCategoryIds(int $postId): array
|
||||
{
|
||||
$categories = get_the_category($postId);
|
||||
|
||||
if (!is_array($categories)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static fn($cat): int => (int) $cat->term_id,
|
||||
$categories
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el post type de un post.
|
||||
*/
|
||||
private function getPostType(int $postId): string
|
||||
{
|
||||
$postType = get_post_type($postId);
|
||||
|
||||
return $postType !== false ? $postType : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
|
||||
use ROITheme\Public\AdsensePlacement\Infrastructure\Api\WordPress\AdsenseVisibilityController;
|
||||
|
||||
/**
|
||||
* Encola los assets JavaScript para el modo JavaScript-First de AdSense.
|
||||
*
|
||||
* Infrastructure Layer - Integra con WordPress asset system.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Infrastructure\Ui
|
||||
*/
|
||||
final class AdsenseAssetsEnqueuer
|
||||
{
|
||||
private const COMPONENT_NAME = 'adsense-placement';
|
||||
private const SCRIPT_HANDLE = 'roi-adsense-visibility';
|
||||
private const SCRIPT_VERSION = '1.0.0';
|
||||
|
||||
public function __construct(
|
||||
private ComponentSettingsRepositoryInterface $settingsRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra el hook para encolar scripts.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encola el script de visibilidad si el modo JS-First esta activo.
|
||||
*/
|
||||
public function enqueueScripts(): void
|
||||
{
|
||||
// Verificar si el modo JS-First esta activo
|
||||
if (!$this->isJavascriptFirstModeEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Solo en frontend, no en admin
|
||||
if (is_admin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scriptPath = $this->getScriptPath();
|
||||
|
||||
// Verificar que el archivo existe
|
||||
if (!file_exists($scriptPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scriptUrl = $this->getScriptUrl();
|
||||
|
||||
wp_enqueue_script(
|
||||
self::SCRIPT_HANDLE,
|
||||
$scriptUrl,
|
||||
[], // Sin dependencias
|
||||
self::SCRIPT_VERSION,
|
||||
true // En footer
|
||||
);
|
||||
|
||||
// Pasar configuracion al script
|
||||
wp_localize_script(self::SCRIPT_HANDLE, 'roiAdsenseConfig', $this->getScriptConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el modo JavaScript-First esta activo.
|
||||
*/
|
||||
private function isJavascriptFirstModeEnabled(): bool
|
||||
{
|
||||
$settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
|
||||
|
||||
$isEnabled = (bool) ($settings['visibility']['is_enabled'] ?? false);
|
||||
$jsFirstMode = (bool) ($settings['behavior']['javascript_first_mode'] ?? false);
|
||||
|
||||
return $isEnabled && $jsFirstMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la ruta fisica del script.
|
||||
*/
|
||||
private function getScriptPath(): string
|
||||
{
|
||||
return get_template_directory() . '/Public/AdsensePlacement/Infrastructure/Ui/Assets/adsense-visibility.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la URL del script.
|
||||
*/
|
||||
private function getScriptUrl(): string
|
||||
{
|
||||
return get_template_directory_uri() . '/Public/AdsensePlacement/Infrastructure/Ui/Assets/adsense-visibility.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la configuracion para pasar al script.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getScriptConfig(): array
|
||||
{
|
||||
$postId = $this->getCurrentPostId();
|
||||
$settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
|
||||
|
||||
return [
|
||||
'endpoint' => rest_url('roi-theme/v1/adsense-placement/visibility'),
|
||||
'postId' => $postId,
|
||||
'nonce' => wp_create_nonce(AdsenseVisibilityController::getNonceAction()),
|
||||
'settingsVersion' => $settings['_meta']['version'] ?? '1.0.0',
|
||||
'debug' => defined('WP_DEBUG') && WP_DEBUG,
|
||||
'featureEnabled' => true,
|
||||
'fallbackStrategy' => 'cached-or-show', // cached-or-show | cached-or-hide | always-show
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el ID del post actual (0 si no es un post singular).
|
||||
*/
|
||||
private function getCurrentPostId(): int
|
||||
{
|
||||
if (is_singular()) {
|
||||
return get_the_ID() ?: 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* ROI Theme - AdSense JavaScript-First Visibility Controller
|
||||
*
|
||||
* Mueve las decisiones de visibilidad de anuncios del servidor (PHP) al cliente (JS)
|
||||
* para permitir compatibilidad con cache de pagina mientras mantiene personalizacion
|
||||
* por usuario.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @see openspec/specs/adsense-javascript-first/spec.md
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const VERSION = '1.0.0';
|
||||
const CACHE_KEY = 'roi_adsense_visibility';
|
||||
const CACHE_VERSION_KEY = 'roi_adsense_settings_version';
|
||||
|
||||
// Configuracion inyectada por PHP via wp_localize_script
|
||||
const config = window.roiAdsenseConfig || {};
|
||||
|
||||
/**
|
||||
* Logger condicional (solo en modo debug)
|
||||
*/
|
||||
function log(message, type = 'log') {
|
||||
if (!config.debug) return;
|
||||
const prefix = '[ROI AdSense v' + VERSION + ']';
|
||||
console[type](prefix, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta si el dispositivo es movil basado en viewport
|
||||
*/
|
||||
function isMobile() {
|
||||
return window.innerWidth < 992;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene decision cacheada de localStorage
|
||||
*/
|
||||
function getCachedDecision() {
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
const cachedVersion = localStorage.getItem(CACHE_VERSION_KEY);
|
||||
|
||||
if (!cached) {
|
||||
log('No hay decision en cache');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Invalidar si la version de settings cambio
|
||||
if (cachedVersion !== config.settingsVersion) {
|
||||
log('Version de settings cambio, invalidando cache');
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
localStorage.removeItem(CACHE_VERSION_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = JSON.parse(cached);
|
||||
|
||||
// Verificar expiracion
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const expiresAt = data.timestamp + data.cache_seconds;
|
||||
|
||||
if (now > expiresAt) {
|
||||
log('Cache expirado');
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
log('Usando decision cacheada: ' + (data.show_ads ? 'MOSTRAR' : 'OCULTAR'));
|
||||
return data;
|
||||
} catch (e) {
|
||||
log('Error leyendo cache: ' + e.message, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda decision en localStorage
|
||||
*/
|
||||
function cacheDecision(decision) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(decision));
|
||||
localStorage.setItem(CACHE_VERSION_KEY, config.settingsVersion);
|
||||
log('Decision cacheada por ' + decision.cache_seconds + 's');
|
||||
} catch (e) {
|
||||
log('Error guardando cache: ' + e.message, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consulta el endpoint REST para obtener decision de visibilidad
|
||||
*/
|
||||
async function fetchVisibilityDecision() {
|
||||
const url = new URL(config.endpoint);
|
||||
url.searchParams.append('post_id', config.postId);
|
||||
|
||||
if (config.nonce) {
|
||||
url.searchParams.append('nonce', config.nonce);
|
||||
}
|
||||
|
||||
log('Consultando endpoint: ' + url.toString());
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa los anuncios (muestra placeholders, carga AdSense)
|
||||
*/
|
||||
function activateAds() {
|
||||
log('Activando anuncios');
|
||||
|
||||
// Remover clase de oculto de los containers
|
||||
document.querySelectorAll('.roi-adsense-placeholder').forEach(function(el) {
|
||||
el.classList.remove('roi-adsense-hidden');
|
||||
el.classList.add('roi-adsense-active');
|
||||
});
|
||||
|
||||
// Disparar evento para que otros scripts puedan reaccionar
|
||||
document.dispatchEvent(new CustomEvent('roiAdsenseActivated', {
|
||||
detail: { version: VERSION }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Desactiva los anuncios (oculta placeholders)
|
||||
*/
|
||||
function deactivateAds(reasons) {
|
||||
log('Desactivando anuncios. Razones: ' + reasons.join(', '));
|
||||
|
||||
// Agregar clase de oculto a los containers
|
||||
document.querySelectorAll('.roi-adsense-placeholder').forEach(function(el) {
|
||||
el.classList.add('roi-adsense-hidden');
|
||||
el.classList.remove('roi-adsense-active');
|
||||
});
|
||||
|
||||
// Disparar evento
|
||||
document.dispatchEvent(new CustomEvent('roiAdsenseDeactivated', {
|
||||
detail: { reasons: reasons, version: VERSION }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica decision de visibilidad
|
||||
*/
|
||||
function applyDecision(decision) {
|
||||
if (decision.show_ads) {
|
||||
activateAds();
|
||||
} else {
|
||||
deactivateAds(decision.reasons || []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja error segun estrategia de fallback configurada
|
||||
*/
|
||||
function handleError(error) {
|
||||
log('Error: ' + error.message, 'error');
|
||||
|
||||
const cached = getCachedDecision();
|
||||
|
||||
switch (config.fallbackStrategy) {
|
||||
case 'cached-or-show':
|
||||
if (cached) {
|
||||
log('Usando cache como fallback');
|
||||
applyDecision(cached);
|
||||
} else {
|
||||
log('Sin cache, mostrando ads por defecto (proteger revenue)');
|
||||
activateAds();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cached-or-hide':
|
||||
if (cached) {
|
||||
log('Usando cache como fallback');
|
||||
applyDecision(cached);
|
||||
} else {
|
||||
log('Sin cache, ocultando ads por defecto');
|
||||
deactivateAds(['fallback_no_cache']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'always-show':
|
||||
log('Fallback: siempre mostrar');
|
||||
activateAds();
|
||||
break;
|
||||
|
||||
default:
|
||||
log('Estrategia desconocida, mostrando ads');
|
||||
activateAds();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Funcion principal de inicializacion
|
||||
*/
|
||||
async function init() {
|
||||
log('Inicializando...');
|
||||
|
||||
// Verificar que el feature este habilitado
|
||||
if (!config.featureEnabled) {
|
||||
log('Feature deshabilitado, usando modo legacy', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
// IMPORTANTE: postId = 0 es valido (paginas de archivo, home, etc.)
|
||||
// Solo validar que endpoint exista y postId no sea undefined/null
|
||||
if (!config.endpoint || config.postId === undefined || config.postId === null) {
|
||||
log('Sin endpoint configurado, activando ads', 'warn');
|
||||
activateAds();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intentar usar cache primero
|
||||
const cached = getCachedDecision();
|
||||
if (cached) {
|
||||
applyDecision(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
// Consultar endpoint
|
||||
try {
|
||||
const decision = await fetchVisibilityDecision();
|
||||
log('Respuesta del servidor: ' + JSON.stringify(decision));
|
||||
|
||||
// Cachear decision
|
||||
cacheDecision(decision);
|
||||
|
||||
// Aplicar decision
|
||||
applyDecision(decision);
|
||||
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Ejecutar cuando el DOM este listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Exponer API publica para debugging
|
||||
window.roiAdsenseVisibility = {
|
||||
version: VERSION,
|
||||
getConfig: function() { return config; },
|
||||
getCachedDecision: getCachedDecision,
|
||||
clearCache: function() {
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
localStorage.removeItem(CACHE_VERSION_KEY);
|
||||
log('Cache limpiado');
|
||||
},
|
||||
forceRefresh: async function() {
|
||||
this.clearCache();
|
||||
await init();
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"component_name": "adsense-placement",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"description": "Control de AdSense y Google Analytics - Con In-Content Ads Avanzado",
|
||||
"groups": {
|
||||
"visibility": {
|
||||
@@ -282,6 +282,13 @@
|
||||
"label": "Ubicaciones en Posts",
|
||||
"priority": 70,
|
||||
"fields": {
|
||||
"javascript_first_mode": {
|
||||
"type": "boolean",
|
||||
"label": "JavaScript-First Mode (Cache Compatible)",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "Mueve decisiones de visibilidad de PHP a JavaScript para compatibilidad con cache de pagina. Permite personalizacion por usuario en sitios cacheados."
|
||||
},
|
||||
"post_top_enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Despues de Featured Image",
|
||||
|
||||
@@ -174,6 +174,15 @@ try {
|
||||
);
|
||||
$youtubeFacadeHooksRegistrar->register();
|
||||
|
||||
// === ADSENSE JAVASCRIPT-FIRST MODE (Plan adsense-javascript-first) ===
|
||||
// Mueve decisiones de visibilidad de PHP a JS para compatibilidad con cache
|
||||
// @see openspec/specs/adsense-javascript-first/spec.md
|
||||
$adsenseJsFirstProvider = new \ROITheme\Public\AdsensePlacement\Infrastructure\Providers\AdsenseJavascriptFirstServiceProvider(
|
||||
$container
|
||||
);
|
||||
$adsenseJsFirstProvider->register();
|
||||
$adsenseJsFirstProvider->boot();
|
||||
|
||||
// === CACHE-FIRST ARCHITECTURE (Plan 1000.01) ===
|
||||
// Hook para plugins externos que necesitan evaluar acceso antes de servir página
|
||||
// @see openspec/specs/cache-first-architecture/spec.md
|
||||
|
||||
545
openspec/specs/adsense-javascript-first/test-plan.md
Normal file
545
openspec/specs/adsense-javascript-first/test-plan.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# Plan de Pruebas: AdSense JavaScript-First Architecture
|
||||
|
||||
> **NOTA IMPORTANTE - PROTOCOLO DE PRUEBAS**
|
||||
>
|
||||
> Las pruebas se ejecutan en el servidor de PRODUCCION.
|
||||
> Si hay algo que corregir, se modifica en LOCAL y luego se despliega.
|
||||
>
|
||||
> **PROHIBIDO**: Modificar codigo directamente en produccion.
|
||||
> **PERMITIDO**: Solo ejecutar pruebas y verificaciones en produccion.
|
||||
>
|
||||
> Flujo correcto:
|
||||
> 1. Ejecutar prueba en produccion
|
||||
> 2. Si falla, corregir en local
|
||||
> 3. Desplegar cambios a produccion
|
||||
> 4. Re-ejecutar prueba
|
||||
|
||||
---
|
||||
|
||||
## Resumen de Pruebas
|
||||
|
||||
| ID | Categoria | Descripcion | Criterio de Aceptacion |
|
||||
|----|-----------|-------------|------------------------|
|
||||
| T01 | Endpoint REST | Endpoint registrado y accesible | HTTP 200 con JSON valido |
|
||||
| T02 | Endpoint REST | Headers anti-cache presentes | Cache-Control, Pragma, Expires |
|
||||
| T03 | Endpoint REST | Parametro post_id requerido | HTTP 400 sin post_id |
|
||||
| T04 | Endpoint REST | post_id=0 valido (archivos/home) | HTTP 200 con post_id=0 |
|
||||
| T05 | Visibilidad | Componente deshabilitado | show_ads=false, reason=component_disabled |
|
||||
| T06 | Visibilidad | Usuario anonimo sin exclusiones | show_ads=true, reasons=[] |
|
||||
| T07 | Visibilidad | Usuario logueado excluido | show_ads=false, reason=logged_in_excluded |
|
||||
| T08 | Visibilidad | Rol excluido | show_ads=false, reason=role_excluded |
|
||||
| T09 | Visibilidad | Post excluido | show_ads=false, reason=post_excluded |
|
||||
| T10 | JavaScript | Script cargado en frontend | roiAdsenseConfig definido |
|
||||
| T11 | JavaScript | Cache localStorage funciona | Datos guardados correctamente |
|
||||
| T12 | JavaScript | Fallback cuando error | Ads se muestran en error |
|
||||
| T13 | Feature Flag | Modo deshabilitado = legacy | No llama endpoint |
|
||||
| T14 | Feature Flag | Modo habilitado = JS-First | Llama endpoint |
|
||||
| T15 | Clean Arch | Value Objects inmutables | No WordPress en Domain |
|
||||
| T16 | Clean Arch | Interface en Domain | AdsenseVisibilityCheckerInterface existe |
|
||||
|
||||
---
|
||||
|
||||
## Pruebas Detalladas
|
||||
|
||||
### T01: Endpoint REST Registrado y Accesible
|
||||
|
||||
**Categoria**: Endpoint REST
|
||||
**Prioridad**: CRITICA
|
||||
**Spec Reference**: Requirement: Endpoint REST Visibility
|
||||
|
||||
**Pasos**:
|
||||
1. Abrir navegador o usar curl
|
||||
2. Acceder a: `https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1`
|
||||
|
||||
**Resultado Esperado**:
|
||||
- HTTP Status: 200
|
||||
- Content-Type: application/json
|
||||
- Body contiene: `show_ads`, `reasons`, `cache_seconds`, `timestamp`
|
||||
|
||||
**Comando de Prueba**:
|
||||
```bash
|
||||
curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T02: Headers Anti-Cache Presentes
|
||||
|
||||
**Categoria**: Endpoint REST
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Headers anti-cache obligatorios
|
||||
|
||||
**Pasos**:
|
||||
1. Hacer request al endpoint
|
||||
2. Verificar headers de respuesta
|
||||
|
||||
**Resultado Esperado**:
|
||||
- `Cache-Control: no-store, no-cache, must-revalidate, max-age=0`
|
||||
- `Pragma: no-cache`
|
||||
- `Expires: Thu, 01 Jan 1970 00:00:00 GMT` o `0`
|
||||
- `Vary: Cookie`
|
||||
|
||||
**Comando de Prueba**:
|
||||
```bash
|
||||
curl -I "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T03: Parametro post_id Requerido
|
||||
|
||||
**Categoria**: Endpoint REST
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Parametros del endpoint
|
||||
|
||||
**Pasos**:
|
||||
1. Hacer request SIN post_id
|
||||
2. Verificar respuesta de error
|
||||
|
||||
**Resultado Esperado**:
|
||||
- HTTP Status: 400 (Bad Request)
|
||||
- Body contiene mensaje de error indicando que post_id es requerido
|
||||
|
||||
**Comando de Prueba**:
|
||||
```bash
|
||||
curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility"
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T04: post_id=0 Valido para Paginas de Archivo
|
||||
|
||||
**Categoria**: Endpoint REST
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Parametros del endpoint (validate_callback >= 0)
|
||||
|
||||
**Pasos**:
|
||||
1. Hacer request con post_id=0
|
||||
2. Verificar que responde correctamente
|
||||
|
||||
**Resultado Esperado**:
|
||||
- HTTP Status: 200
|
||||
- Body contiene decision de visibilidad valida
|
||||
|
||||
**Comando de Prueba**:
|
||||
```bash
|
||||
curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=0"
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T05: Componente Deshabilitado Retorna False
|
||||
|
||||
**Categoria**: Visibilidad
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Componente deshabilitado
|
||||
|
||||
**Pre-condicion**:
|
||||
- Deshabilitar componente en admin (is_enabled = false)
|
||||
|
||||
**Pasos**:
|
||||
1. Deshabilitar adsense-placement en admin
|
||||
2. Hacer request al endpoint
|
||||
3. Verificar respuesta
|
||||
|
||||
**Resultado Esperado**:
|
||||
```json
|
||||
{
|
||||
"show_ads": false,
|
||||
"reasons": ["component_disabled"],
|
||||
"cache_seconds": 3600
|
||||
}
|
||||
```
|
||||
|
||||
**Comando de Prueba**:
|
||||
```bash
|
||||
curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T06: Usuario Anonimo Sin Exclusiones Ve Ads
|
||||
|
||||
**Categoria**: Visibilidad
|
||||
**Prioridad**: CRITICA
|
||||
**Spec Reference**: Scenario: Usuario anonimo sin exclusiones
|
||||
|
||||
**Pre-condicion**:
|
||||
- Componente habilitado
|
||||
- javascript_first_mode habilitado
|
||||
- Sin exclusiones configuradas
|
||||
- No estar logueado
|
||||
|
||||
**Pasos**:
|
||||
1. Abrir navegador en modo incognito
|
||||
2. Acceder a un post del sitio
|
||||
3. Verificar respuesta del endpoint
|
||||
|
||||
**Resultado Esperado**:
|
||||
```json
|
||||
{
|
||||
"show_ads": true,
|
||||
"reasons": [],
|
||||
"cache_seconds": 60
|
||||
}
|
||||
```
|
||||
|
||||
**Comando de Prueba**:
|
||||
```bash
|
||||
curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T07: Usuario Logueado Excluido
|
||||
|
||||
**Categoria**: Visibilidad
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Usuario logueado excluido
|
||||
|
||||
**Pre-condicion**:
|
||||
- Activar "Ocultar para usuarios logueados" en admin
|
||||
|
||||
**Pasos**:
|
||||
1. Loguearse en WordPress
|
||||
2. Copiar cookies de sesion
|
||||
3. Hacer request con cookies
|
||||
|
||||
**Resultado Esperado**:
|
||||
```json
|
||||
{
|
||||
"show_ads": false,
|
||||
"reasons": ["logged_in_excluded"],
|
||||
"cache_seconds": 300
|
||||
}
|
||||
```
|
||||
|
||||
**Verificacion Manual**:
|
||||
1. Loguearse en wp-admin
|
||||
2. Visitar un post en el frontend
|
||||
3. Abrir DevTools > Network
|
||||
4. Buscar request a `/visibility`
|
||||
5. Verificar respuesta
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T08: Rol Excluido
|
||||
|
||||
**Categoria**: Visibilidad
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Rol de usuario excluido
|
||||
|
||||
**Pre-condicion**:
|
||||
- Agregar "administrator" a roles excluidos en admin
|
||||
|
||||
**Pasos**:
|
||||
1. Loguearse como administrator
|
||||
2. Visitar un post
|
||||
3. Verificar respuesta del endpoint
|
||||
|
||||
**Resultado Esperado**:
|
||||
```json
|
||||
{
|
||||
"show_ads": false,
|
||||
"reasons": ["role_excluded"],
|
||||
"cache_seconds": 300
|
||||
}
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T09: Post Excluido por ID
|
||||
|
||||
**Categoria**: Visibilidad
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Post excluido por ID
|
||||
|
||||
**Pre-condicion**:
|
||||
- Agregar un ID de post a "IDs de posts excluidos" en admin
|
||||
|
||||
**Pasos**:
|
||||
1. Anotar el ID del post excluido (ej: 123)
|
||||
2. Hacer request con ese post_id
|
||||
3. Verificar respuesta
|
||||
|
||||
**Resultado Esperado**:
|
||||
```json
|
||||
{
|
||||
"show_ads": false,
|
||||
"reasons": ["post_excluded"],
|
||||
"cache_seconds": 60
|
||||
}
|
||||
```
|
||||
|
||||
**Comando de Prueba**:
|
||||
```bash
|
||||
curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=123"
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T10: Script JavaScript Cargado
|
||||
|
||||
**Categoria**: JavaScript
|
||||
**Prioridad**: CRITICA
|
||||
**Spec Reference**: Scenario: Configuracion via wp_localize_script
|
||||
|
||||
**Pre-condicion**:
|
||||
- javascript_first_mode habilitado
|
||||
|
||||
**Pasos**:
|
||||
1. Visitar un post en el frontend
|
||||
2. Abrir DevTools > Console
|
||||
3. Escribir: `window.roiAdsenseConfig`
|
||||
|
||||
**Resultado Esperado**:
|
||||
- Objeto definido con propiedades:
|
||||
- `endpoint`: URL del endpoint REST
|
||||
- `postId`: ID del post actual
|
||||
- `nonce`: String no vacio
|
||||
- `featureEnabled`: true
|
||||
- `debug`: boolean
|
||||
|
||||
**Verificacion Alternativa**:
|
||||
```javascript
|
||||
// En consola del navegador
|
||||
console.log(window.roiAdsenseConfig);
|
||||
console.log(typeof window.roiAdsenseVisibility);
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T11: Cache localStorage Funciona
|
||||
|
||||
**Categoria**: JavaScript
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Cache en localStorage
|
||||
|
||||
**Pasos**:
|
||||
1. Visitar un post (primera vez)
|
||||
2. Abrir DevTools > Application > Local Storage
|
||||
3. Buscar key `roi_adsense_visibility`
|
||||
4. Recargar pagina
|
||||
5. Verificar en Network que NO hay nueva llamada al endpoint
|
||||
|
||||
**Resultado Esperado**:
|
||||
- localStorage contiene:
|
||||
```json
|
||||
{
|
||||
"show_ads": true,
|
||||
"reasons": [],
|
||||
"timestamp": 1733900000,
|
||||
"cache_seconds": 60
|
||||
}
|
||||
```
|
||||
- Segunda carga NO hace request al endpoint (usa cache)
|
||||
|
||||
**Verificacion en Consola**:
|
||||
```javascript
|
||||
localStorage.getItem('roi_adsense_visibility');
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T12: Fallback en Error de Red
|
||||
|
||||
**Categoria**: JavaScript
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Fallback strategy cached-or-show
|
||||
|
||||
**Pasos**:
|
||||
1. Limpiar localStorage
|
||||
2. Abrir DevTools > Network
|
||||
3. Habilitar "Offline" mode
|
||||
4. Visitar un post
|
||||
5. Verificar comportamiento
|
||||
|
||||
**Resultado Esperado**:
|
||||
- Los ads se muestran (fallback = show)
|
||||
- No hay error en consola (error manejado gracefully)
|
||||
|
||||
**Verificacion Alternativa**:
|
||||
```javascript
|
||||
// Limpiar cache
|
||||
window.roiAdsenseVisibility.clearCache();
|
||||
// Recargar con network offline
|
||||
```
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T13: Feature Flag Deshabilitado = Modo Legacy
|
||||
|
||||
**Categoria**: Feature Flag
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Feature flag deshabilitado
|
||||
|
||||
**Pre-condicion**:
|
||||
- Deshabilitar javascript_first_mode en admin
|
||||
|
||||
**Pasos**:
|
||||
1. Deshabilitar javascript_first_mode
|
||||
2. Visitar un post
|
||||
3. Verificar en Network que NO hay llamada al endpoint
|
||||
|
||||
**Resultado Esperado**:
|
||||
- `roiAdsenseConfig.featureEnabled` = false
|
||||
- No hay request a `/visibility` endpoint
|
||||
- Ads se muestran inmediatamente (modo legacy)
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T14: Feature Flag Habilitado = JS-First
|
||||
|
||||
**Categoria**: Feature Flag
|
||||
**Prioridad**: ALTA
|
||||
**Spec Reference**: Scenario: Feature flag habilitado
|
||||
|
||||
**Pre-condicion**:
|
||||
- Habilitar javascript_first_mode en admin
|
||||
|
||||
**Pasos**:
|
||||
1. Habilitar javascript_first_mode
|
||||
2. Limpiar cache (localStorage y pagina)
|
||||
3. Visitar un post
|
||||
4. Verificar en Network que SI hay llamada al endpoint
|
||||
|
||||
**Resultado Esperado**:
|
||||
- `roiAdsenseConfig.featureEnabled` = true
|
||||
- Request a `/visibility` endpoint presente
|
||||
- Ads se muestran/ocultan segun respuesta
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T15: Value Objects Sin Dependencias WordPress
|
||||
|
||||
**Categoria**: Clean Architecture
|
||||
**Prioridad**: MEDIA
|
||||
**Spec Reference**: Scenario: Value Object VisibilityDecision en Domain
|
||||
|
||||
**Verificacion**:
|
||||
Revisar que los archivos NO contengan funciones de WordPress:
|
||||
|
||||
**Archivos a verificar**:
|
||||
- `Domain/ValueObjects/UserContext.php`
|
||||
- `Domain/ValueObjects/VisibilityDecision.php`
|
||||
- `Domain/ValueObjects/AdsenseSettings.php`
|
||||
- `Domain/Contracts/AdsenseVisibilityCheckerInterface.php`
|
||||
- `Application/UseCases/CheckAdsenseVisibilityUseCase.php`
|
||||
|
||||
**Resultado Esperado**:
|
||||
- Sin `get_`, `wp_`, `is_user_logged_in`, `WP_*` classes
|
||||
- Solo PHP puro y tipos del proyecto
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
### T16: Interface en Domain
|
||||
|
||||
**Categoria**: Clean Architecture
|
||||
**Prioridad**: MEDIA
|
||||
**Spec Reference**: Scenario: Interface en Domain
|
||||
|
||||
**Verificacion**:
|
||||
El archivo `Domain/Contracts/AdsenseVisibilityCheckerInterface.php` debe:
|
||||
- Existir en la ruta correcta
|
||||
- Definir metodo `check(int $postId, UserContext $userContext): VisibilityDecision`
|
||||
- NO referenciar WordPress
|
||||
|
||||
**Estado**: [ ] Pendiente
|
||||
**Resultado**:
|
||||
**Notas**:
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Despliegue Pre-Pruebas
|
||||
|
||||
Antes de ejecutar las pruebas, verificar:
|
||||
|
||||
- [ ] Codigo desplegado a produccion via FTP/SSH
|
||||
- [ ] Cache de pagina limpiado
|
||||
- [ ] javascript_first_mode habilitado en admin
|
||||
- [ ] Componente adsense-placement habilitado
|
||||
- [ ] Schema sincronizado en BD (campo javascript_first_mode existe)
|
||||
|
||||
---
|
||||
|
||||
## Registro de Ejecucion
|
||||
|
||||
| Fecha | Tester | Pruebas Ejecutadas | Pasadas | Fallidas | Notas |
|
||||
|-------|--------|-------------------|---------|----------|-------|
|
||||
| | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## Defectos Encontrados
|
||||
|
||||
| ID | Prueba | Descripcion | Severidad | Estado | Correccion |
|
||||
|----|--------|-------------|-----------|--------|------------|
|
||||
| | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## Historial de Versiones
|
||||
|
||||
| Version | Fecha | Cambios |
|
||||
|---------|-------|---------|
|
||||
| 1.0 | 2025-12-11 | Plan inicial basado en spec v1.5 |
|
||||
Reference in New Issue
Block a user