HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/config/ads.php
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Mock Mode
    |--------------------------------------------------------------------------
    | When true, all platform API calls route to MockAdPlatformAdapter.
    | Use for development and E2E testing without real OAuth credentials.
    |
    | Checked EXCLUSIVELY in AdPlatformManager::resolveRawAdapter(). Service
    | code must NEVER read this directly — the decorator chain makes the
    | choice transparent.
    */
    'mock_mode' => env('ADS_MOCK_MODE', false),

    /*
    |--------------------------------------------------------------------------
    | Platform OAuth Credentials + Runtime Tunables
    |--------------------------------------------------------------------------
    | Credentials can be set via .env OR via admin settings (Admin > Settings > Ads).
    | Admin settings take precedence via setting() helper where supported.
    |
    | Each platform block also carries `rate_limits`, `circuit_breaker`, `retry`
    | and `http` sub-arrays consumed by the decorator chain. Keep these tuned
    | per-platform because each API enforces different quotas.
    */
    'platforms' => [
        'meta' => [
            'app_id' => env('META_ADS_APP_ID'),
            'app_secret' => env('META_ADS_APP_SECRET'),
            'api_version' => env('META_ADS_API_VERSION', 'v18.0'),
            'base_url' => env('META_ADS_BASE_URL', 'https://graph.facebook.com'),
            'scopes' => ['ads_management', 'ads_read', 'business_management', 'leads_retrieval'],
            'rate_limits' => [
                // Meta enforces ~200 calls/hour/user — stay under with a margin
                'per_customer' => ['hits' => 180, 'per_seconds' => 3600],
                'global'       => ['hits' => 6000, 'per_seconds' => 3600],
            ],
            'circuit_breaker' => [
                'failure_threshold' => 5,
                'cooldown_seconds'  => 30,
            ],
            'retry' => [
                'max_attempts'  => 3,
                'base_delay_ms' => 2000,
            ],
            'http' => [
                'timeout_seconds'         => 30,
                'connect_timeout_seconds' => 5,
            ],
            // Meta test ad account — no real spend, safe for contract tests (R12)
            'test_ad_account_id' => env('META_TEST_AD_ACCOUNT_ID'),
        ],

        'google' => [
            'client_id'       => env('GOOGLE_ADS_CLIENT_ID'),
            'client_secret'   => env('GOOGLE_ADS_CLIENT_SECRET'),
            'developer_token' => env('GOOGLE_ADS_DEVELOPER_TOKEN'),
            'api_version'     => env('GOOGLE_ADS_API_VERSION', 'v16'),
            'scopes'          => ['https://www.googleapis.com/auth/adwords'],
            'rate_limits' => [
                'per_customer' => ['hits' => 100, 'per_seconds' => 60],   // Google: 100/min/account
                'global'       => ['hits' => 2000, 'per_seconds' => 60],
            ],
            'circuit_breaker' => ['failure_threshold' => 5, 'cooldown_seconds' => 30],
            'retry'           => ['max_attempts' => 3, 'base_delay_ms' => 2000],
            'http'            => ['timeout_seconds' => 30, 'connect_timeout_seconds' => 5],
            'test_account_id' => env('GOOGLE_ADS_TEST_ACCOUNT_ID'),
            // R15.2 — Google Merchant Center identity. `merchant_id` is
            // distinct from the Google Ads `customer_id` and lives on a
            // separate API surface (`shoppingcontent.googleapis.com`).  When
            // null, GoogleAdPlatform::createCatalog throws a clear
            // ValidationException instead of silently mock-fallbacking.
            'merchant_id'      => env('GOOGLE_MERCHANT_ID'),
            'content_country'  => env('GOOGLE_MERCHANT_CONTENT_COUNTRY', 'US'),
            'content_language' => env('GOOGLE_MERCHANT_CONTENT_LANGUAGE', 'en'),
        ],

        'tiktok' => [
            'app_id'      => env('TIKTOK_ADS_APP_ID'),
            'app_secret'  => env('TIKTOK_ADS_APP_SECRET'),
            'api_version' => env('TIKTOK_ADS_API_VERSION', 'v1.3'),
            'base_url'    => env('TIKTOK_ADS_BASE_URL', 'https://business-api.tiktok.com'),
            'scopes'      => [],
            'rate_limits' => [
                'per_customer' => ['hits' => 60, 'per_seconds' => 60],    // TikTok: 60/min/token
                'global'       => ['hits' => 3000, 'per_seconds' => 60],
            ],
            'circuit_breaker' => ['failure_threshold' => 5, 'cooldown_seconds' => 30],
            'retry'           => ['max_attempts' => 3, 'base_delay_ms' => 2000],
            'http'            => ['timeout_seconds' => 30, 'connect_timeout_seconds' => 5],
        ],

        'linkedin' => [
            'client_id'     => env('LINKEDIN_ADS_CLIENT_ID'),
            'client_secret' => env('LINKEDIN_ADS_CLIENT_SECRET'),
            'api_version'   => env('LINKEDIN_ADS_API_VERSION', '202404'),
            'base_url'      => env('LINKEDIN_ADS_BASE_URL', 'https://api.linkedin.com'),
            'scopes'        => ['r_ads', 'rw_ads', 'r_ads_reporting'],
            'rate_limits' => [
                'per_customer' => ['hits' => 100, 'per_seconds' => 3600],
                'global'       => ['hits' => 5000, 'per_seconds' => 3600],
            ],
            'circuit_breaker' => ['failure_threshold' => 5, 'cooldown_seconds' => 30],
            'retry'           => ['max_attempts' => 3, 'base_delay_ms' => 2000],
            'http'            => ['timeout_seconds' => 30, 'connect_timeout_seconds' => 5],
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Queues
    |--------------------------------------------------------------------------
    | IMPORTANT — SAFE DEFAULT: ads jobs run on the `default` queue like every
    | other Laravel job. This matches Acelle's out-of-the-box supervisor setup
    | (`php artisan queue:work` with no --queue arg) so existing installs keep
    | working after upgrade with zero ops action.
    |
    | Customers who want queue isolation (ads outage must not block email
    | sends) opt in via .env:
    |
    |   ADS_QUEUE_NAME=ads
    |   ADS_WEBHOOKS_QUEUE_NAME=ads-webhooks
    |
    | Then update supervisor to watch those queues:
    |   php artisan queue:work --queue=ads-webhooks,ads,default
    |
    | See docs/ads/review/PROGRESS.md "Feature-flag pattern" for the broader
    | rule — every behavior change in Ads hardening phases must be opt-in.
    */
    'queues' => [
        'default'        => env('ADS_QUEUE_NAME', 'default'),
        'webhooks'       => env('ADS_WEBHOOKS_QUEUE_NAME', 'default'),
        'reconciliation' => env('ADS_RECONCILE_QUEUE_NAME', 'default'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Sync Intervals (cron)
    |--------------------------------------------------------------------------
    */
    'metrics_sync_interval'  => env('ADS_METRICS_SYNC_INTERVAL', 15),     // minutes
    'token_refresh_interval' => env('ADS_TOKEN_REFRESH_INTERVAL', 30),    // minutes
    'health_check_interval'  => env('ADS_HEALTH_CHECK_INTERVAL', 60),     // minutes
    'reconcile_interval'     => env('ADS_RECONCILE_INTERVAL', 5),         // minutes (recovery of orphan publish logs)

    /*
    |--------------------------------------------------------------------------
    | Data Retention (GDPR / compliance — R11)
    |--------------------------------------------------------------------------
    | Artisan command `ads:prune-retention` (cron daily at 03:00) enforces
    | these TTLs. Tune per regulatory requirements; zero disables.
    */
    'retention' => [
        'ad_leads_days'          => (int) env('ADS_LEADS_RETENTION_DAYS', 180),
        'ad_metrics_days'        => (int) env('ADS_METRICS_RETENTION_DAYS', 730),
        'ad_activity_logs_days'  => (int) env('ADS_ACTIVITY_LOG_RETENTION_DAYS', 365),
        'ad_publish_logs_days'   => (int) env('ADS_PUBLISH_LOG_RETENTION_DAYS', 180),
    ],

    /*
    |--------------------------------------------------------------------------
    | Emergency Kill Switches
    |--------------------------------------------------------------------------
    | Admin (or ops via .env) can disable any single platform instantly.
    | AdPlatformManager::guardKillSwitch() throws PlatformKillSwitchException
    | BEFORE any API call — zero chance of leaking through.
    |
    | Customer UI shows a suspension banner; campaigns remain intact, only
    | outbound calls are blocked.
    */
    'kill_switches' => [
        'meta'     => env('ADS_KILL_META', false),
        'google'   => env('ADS_KILL_GOOGLE', false),
        'tiktok'   => env('ADS_KILL_TIKTOK', false),
        'linkedin' => env('ADS_KILL_LINKEDIN', false),

        // Optional reason strings surfaced to admin + customer banner
        'meta_reason'     => env('ADS_KILL_META_REASON'),
        'google_reason'   => env('ADS_KILL_GOOGLE_REASON'),
        'tiktok_reason'   => env('ADS_KILL_TIKTOK_REASON'),
        'linkedin_reason' => env('ADS_KILL_LINKEDIN_REASON'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Webhooks (R10)
    |--------------------------------------------------------------------------
    */
    'webhooks' => [
        'meta_verify_token_env'         => 'META_WEBHOOK_VERIFY_TOKEN',
        'meta_verify_token'             => env('META_WEBHOOK_VERIFY_TOKEN', ''),
        'signature_tolerance_seconds'   => 300,
        'replay_protection_ttl_seconds' => 300,

        // R10 — enforce HMAC signature on inbound webhook POSTs.
        // OFF (default) = observation mode: signature is verified + logged
        // but invalid-signature requests still pass through so staging
        // can smoke-test against Meta's sample events (which don't sign
        // with the production secret).  Replay protection always runs
        // regardless of this flag — dedup is always safe.
        // ON = reject 401 on signature mismatch.
        'enforce_signature' => env('ADS_WEBHOOKS_ENFORCE_SIGNATURE', false),
    ],

    /*
    |--------------------------------------------------------------------------
    | Observability (R9)
    |--------------------------------------------------------------------------
    | Dedicated log channel. Sample successful adapter calls at 10% to avoid
    | log spam; always log errors at 100%.
    */
    'observability' => [
        'log_channel'         => env('ADS_LOG_CHANNEL', 'stack'),
        'sample_rate_success' => (float) env('ADS_LOG_SAMPLE_SUCCESS', 0.1),
        'sample_rate_error'   => (float) env('ADS_LOG_SAMPLE_ERROR', 1.0),
    ],

    /*
    |--------------------------------------------------------------------------
    | Compliance (R11)
    |--------------------------------------------------------------------------
    */
    'compliance' => [
        // Remove subscriber from all ad audiences when they unsubscribe from email
        'gdpr_remove_on_unsubscribe' => env('ADS_GDPR_REMOVE_ON_UNSUB', true),
        // Audit admin when they view lead PII data
        'admin_audit_lead_views' => env('ADS_AUDIT_LEAD_VIEWS', true),
    ],

    /*
    |--------------------------------------------------------------------------
    | Automation Rules (R13)
    |--------------------------------------------------------------------------
    | Customer-defined rules that fire actions (add/remove from audience,
    | start/pause campaign) on subscriber events (unsubscribe, open, click,
    | purchase).
    |
    |   enabled — master kill switch.  When false, the listener still
    |     receives events but exits early without dispatching jobs.
    |   throttle_per_minute — max actions a single rule can fire in any
    |     60-second window.  Hard-stop guard against runaway rules; a
    |     misconfigured rule that fires on every email open can flood the
    |     queue otherwise.
    |   default_dry_run — initial value of the rule.dry_run column when
    |     a customer creates a new rule via the wizard.  Default true so
    |     new rules don't silently mutate audiences before the customer
    |     reviews the simulation log.
    */
    'automation' => [
        'enabled' => env('ADS_AUTOMATION_ENABLED', true),
        'throttle_per_minute' => (int) env('ADS_AUTOMATION_THROTTLE_PER_MINUTE', 10),
        'default_dry_run' => env('ADS_AUTOMATION_DEFAULT_DRY_RUN', true),
        // Lock TTL for the per-rule concurrency guard (ads_aut_running:{id}).
        // Longer than any single action's expected runtime; releases on job
        // completion / failure / timeout.  Crash-safe: TTL expires
        // automatically if the worker dies mid-execution.
        'concurrency_lock_ttl_seconds' => (int) env('ADS_AUTOMATION_LOCK_TTL', 300),
    ],

    /*
    |--------------------------------------------------------------------------
    | Product Catalog Sync (R15)
    |--------------------------------------------------------------------------
    | validate_images — when true, ProductImageValidator HEAD-checks every
    |   product image URL before sending to the adapter.  Catches broken
    |   URLs upfront so the per-product error message says "image
    |   unreachable" instead of the platform's cryptic batch error.  Default
    |   ON in production; staging fixtures may want to disable to avoid
    |   real HTTP calls during unit tests.
    | image_timeout_ms — HEAD request timeout per image URL.  Bumping this
    |   slows full-catalog validation linearly; default 3s tolerates slow
    |   CDNs without dragging out a 1000-product sync.
    */
    'catalog' => [
        'validate_images' => env('ADS_CATALOG_VALIDATE_IMAGES', true),
        'image_timeout_ms' => (int) env('ADS_CATALOG_IMAGE_TIMEOUT_MS', 3000),
        // R15.1 — minimum hours since last_synced_at before the daily
        // sweeper re-dispatches a SyncAdProductCatalog job.  Lower for
        // staging where sources change quickly; higher to ease platform
        // quota pressure.  Manual catalogs are always skipped — they
        // only sync on customer button-press.
        'sweep_min_age_hours' => (int) env('ADS_CATALOG_SWEEP_MIN_AGE_HOURS', 24),
    ],

    /*
    |--------------------------------------------------------------------------
    | Sandbox contract tests (R12)
    |--------------------------------------------------------------------------
    | Contract tests hit real platform sandboxes to detect API schema drift.
    | Disabled by default — customers flip this for pre-release validation.
    */
    'sandbox' => [
        'enabled' => env('ADS_SANDBOX_TESTS', false),
    ],

    /*
    |--------------------------------------------------------------------------
    | Hardening Feature Flags (R3+)
    |--------------------------------------------------------------------------
    | Risky behavioral changes ship behind flags, default OFF.  Customers opt
    | in via .env after staging verification.
    |
    | use_session_pattern (R3) — when true, services call
    |   $manager->session($platform)->chain->method()
    | through the full 6-decorator chain.  When false, legacy direct
    | adapter calls + inline mock-mode branches execute unchanged.
    |
    | See docs/ads/review/PROGRESS.md "Feature-flag pattern" for rollout rules.
    */
    'hardening' => [
        'use_session_pattern' => env('ADS_USE_SESSION_PATTERN', false),
        // R4 flag — when true, MetaAdPlatform calls the real Facebook Graph
        // API for the 10 R1-extended methods (publishCampaign, pullMetrics,
        // syncAudience, etc.).  When false (default), those methods delegate
        // to MockAdPlatform so customers without mock mode keep a working UX
        // while we validate the real implementation in staging.  OAuth +
        // health check stay real regardless — they're the foundation that
        // existing customers already rely on.
        'real_meta_enabled' => env('ADS_REAL_META_ENABLED', false),
        // R12.1 flag — when true, GoogleAdPlatform calls the real Google Ads
        // REST API v17 for the 10 R1-extended methods.  Same flag-OFF mock
        // delegation as Meta's flag.  Requires ADS_GOOGLE_DEVELOPER_TOKEN +
        // approved Google Ads Developer Token tier (basic/standard).
        'real_google_enabled' => env('ADS_REAL_GOOGLE_ENABLED', false),
        // R12.2 flag — when true, TikTokAdPlatform calls the real TikTok
        // Business API v1.3 for the 10 R1-extended methods.  Same flag-OFF
        // mock delegation as Meta + Google flags.  Requires the customer's
        // TikTok for Business app to be approved for production (sandbox
        // apps return placeholder data even with the flag ON).  Catalog
        // methods throw `LogicException` with R15 hint when ON.
        'real_tiktok_enabled' => env('ADS_REAL_TIKTOK_ENABLED', false),
        // R12.3 flag — when true, LinkedInAdPlatform calls the real LinkedIn
        // Marketing API (versioned `/rest/` surface) for the 10 R1-extended
        // methods.  Same flag-OFF mock delegation pattern.  Requires
        // Marketing Developer Platform (MDP) approval — the customer's
        // LinkedIn app must have the rw_ads / r_ads_reporting scopes granted
        // (Lead Sync API access is a separate approval; fetchLeads will
        // surface the access error if the customer skipped it).  Catalog
        // methods throw `LogicException` with R15 hint when ON.
        'real_linkedin_enabled' => env('ADS_REAL_LINKEDIN_ENABLED', false),
        // R15.0 flag — when true, AdProductCatalogService::sync routes to
        // the adapter chain (`$manager->session($platform)->chain->createCatalog
        // / syncCatalog`) for any catalog with an `ad_platform_id` binding.
        // When false (default), the legacy mock-fixture path runs unchanged
        // — existing customers see no behavior change until they explicitly
        // opt in via `.env`.  Per-platform real-API flags (Meta R4, Google
        // R15.2, TikTok R15.3, LinkedIn R15.4) gate the actual HTTP calls
        // inside each adapter; this flag only gates the service-level
        // routing decision.
        'real_catalog_sync_enabled' => env('ADS_REAL_CATALOG_SYNC_ENABLED', false),
        // R15.2 flag — when true, GoogleAdPlatform routes catalog calls to
        // the real `shoppingcontent.googleapis.com` Merchant Center API.
        // Independent from `real_google_enabled` (Ads side); a customer
        // can run real Ads + mock catalog or real catalog + mock Ads.
        // Requires `GOOGLE_MERCHANT_ID` set and the customer reconnected
        // with `auth/content` scope.  Flag-OFF delegates to MockAdPlatform.
        'real_google_catalog_enabled' => env('ADS_REAL_GOOGLE_CATALOG_ENABLED', false),
        // R15.3 flag — when true, TikTokAdPlatform routes catalog calls to
        // the real Business API Catalog Manager endpoints (/catalog/create/
        // and /catalog/product/upload/).  Independent from `real_tiktok_
        // enabled` (Ads side).  No extra credentials beyond the existing
        // TikTok app + advertiser binding.  Flag-OFF delegates to MockAdPlatform.
        'real_tiktok_catalog_enabled' => env('ADS_REAL_TIKTOK_CATALOG_ENABLED', false),
        // R15.4 flag — when true, LinkedInAdPlatform routes catalog calls to
        // the real Marketing API dynamic-ads endpoints (/rest/dataItemCatalogs
        // and /rest/dataItems?action=batchCreate).  IMPORTANT: LinkedIn
        // Dynamic Ads access requires Marketing Developer Platform (MDP)
        // approval beyond the standard `rw_ads` scope.  Customers without
        // MDP Dynamic Ads tier will see 403 PERMISSION_DENIED on every
        // catalog call — adapter surfaces this as a ValidationException
        // with explicit reconnect/onboarding hint.  Flag-OFF delegates to
        // MockAdPlatform.  Independent from `real_linkedin_enabled`.
        'real_linkedin_catalog_enabled' => env('ADS_REAL_LINKEDIN_CATALOG_ENABLED', false),
    ],

    /*
    |--------------------------------------------------------------------------
    | Rate-limit + circuit breaker enforcement flags (R8)
    |--------------------------------------------------------------------------
    | Default behavior is OBSERVATION mode — decorators record bucket hits
    | and failure counters in Cache but never throw.  Customers verify
    | Horizon / log volume in staging, then flip to ENFORCE mode in .env:
    |
    |   ADS_RATE_LIMITS_ENFORCE=true       # RateLimitDecorator throws
    |                                         RateLimitException on bucket
    |                                         exhaustion.
    |   ADS_CIRCUIT_BREAKER_ENFORCE=true   # CircuitBreakerDecorator throws
    |                                         CircuitOpenException when a
    |                                         platform trips the failure
    |                                         threshold.
    |
    | Observation-mode telemetry is cheap; enforcement-mode changes customer-
    | facing behavior.  Keep defaults OFF.
    */
    'rate_limits_enforce' => env('ADS_RATE_LIMITS_ENFORCE', false),
    'circuit_breaker_enforce' => env('ADS_CIRCUIT_BREAKER_ENFORCE', false),

    /*
    |--------------------------------------------------------------------------
    | Mock (dev/test only)
    |--------------------------------------------------------------------------
    | Opt-in N-th-call error simulation used by contract tests (R8/R12).
    | See MockAdPlatform::maybeSimulateTransientError.  Unset by default.
    */
    'mock' => [
        // 'error_simulation' => ['every' => 10, 'exception' => 'rate_limit'],
    ],

];