{
  "landingPage": "/wp-admin/admin.php?page=fswa-webhook-actions#/webhooks",
  "preferredVersions": {
    "php": "8.2",
    "wp": "latest"
  },
  "features": {
    "networking": true
  },
  "steps": [
    {
      "step": "installPlugin",
      "pluginZipFile": {
        "resource": "wordpress.org/plugins",
        "slug": "flowsystems-webhook-actions"
      }
    },
    {
      "step": "activatePlugin",
      "pluginPath": "flowsystems-webhook-actions/flowsystems-webhook-actions.php"
    },
    {
      "step": "installPlugin",
      "pluginZipFile": {
        "resource": "wordpress.org/plugins",
        "slug": "woocommerce"
      }
    },
    {
      "step": "activatePlugin",
      "pluginPath": "woocommerce/woocommerce.php"
    },
    {
      "step": "login",
      "username": "admin",
      "password": "password"
    },
    {
      "step": "runPHP",
      "code": "<?php\n/**\n * Seed: a SYNCHRONOUS webhook on the `profile_update` trigger, delivering to\n * httpbin, with a field mapping that strips credentials from the payload.\n *\n * Runs once (guarded by empty(getAll())). The profile_update payload is the RAW\n * hook args, so the password / activation key live at:\n *   args.1.data.* = old WP_User object   (3rd-arg userdata is args.2.*)\n *   args.2.*      = new userdata array\n * We intentionally do NOT seed example_payload — the first real dispatch\n * auto-captures the true payload shape for the admin field picker, and\n * SchemaRepository::upsert() preserves this field_mapping through that capture.\n */\nrequire_once '/wordpress/wp-load.php';\n\nif ( ! class_exists( '\\FlowSystems\\WebhookActions\\Repositories\\WebhookRepository' ) ) {\n    return;\n}\n\n$repo = new \\FlowSystems\\WebhookActions\\Repositories\\WebhookRepository();\n\n// Idempotent: only seed on a fresh install.\nif ( ! empty( $repo->getAll() ) ) {\n    return;\n}\n\n$webhookId = $repo->create([\n    'name'           => 'Demo — Profile Update → httpbin',\n    'endpoint_url'   => 'https://httpbin.org/post',\n    'http_method'    => 'POST',\n    'is_synchronous' => 1,\n    'is_enabled'     => 1,\n    'triggers'       => ['profile_update'],\n]);\n\nif ( $webhookId && class_exists( '\\FlowSystems\\WebhookActions\\Repositories\\SchemaRepository' ) ) {\n    $schemaRepo = new \\FlowSystems\\WebhookActions\\Repositories\\SchemaRepository();\n    $schemaRepo->upsert( (int) $webhookId, 'profile_update', [\n        'field_mapping' => [\n            'mappings'        => [],\n            'excluded'        => [\n                'args.1.data.user_pass',\n                'args.1.data.user_activation_key',\n                'args.2.user_pass',\n                'args.2.user_activation_key',\n            ],\n            'includeUnmapped' => true,\n        ],\n    ] );\n}"
    },
    {
      "step": "runPHP",
      "code": "<?php\n/**\n * Seed: create an example subscriber and update their profile, which fires the\n * `profile_update` trigger and produces one real (synchronous) delivery + log,\n * so the Logs view is populated on landing.\n *\n * This MUST be a separate runPHP step (= separate request) from the webhook seed\n * above: the trigger listener is attached on `init` from the stored triggers, so\n * the webhook + trigger row must already exist in the DB before this request runs\n * wp_update_user(). wp_insert_user() does NOT fire profile_update; wp_update_user()\n * on an existing user does.\n */\nrequire_once '/wordpress/wp-load.php';\n\n$uid = username_exists( 'jane.customer' );\n\nif ( ! $uid ) {\n    $uid = wp_insert_user([\n        'user_login'  => 'jane.customer',\n        'user_pass'   => wp_generate_password(),\n        'user_email'  => 'jane.customer@example.com',\n        'first_name'  => 'Jane',\n        'last_name'   => 'Customer',\n        'role'        => 'subscriber',\n        'description' => 'Example customer account for the webhook demo.',\n    ]);\n}\n\nif ( $uid && ! is_wp_error( $uid ) ) {\n    wp_update_user([\n        'ID'          => $uid,\n        'first_name'  => 'Jane',\n        'last_name'   => 'Customer (updated)',\n        'description' => 'Profile updated by the live preview to fire a synchronous webhook delivery.',\n    ]);\n}"
    },
    {
      "step": "runPHP",
      "code": "<?php\n/**\n * Seed: a WooCommerce \"order fulfilment pipeline\" that showcases CONDITIONS +\n * CHAINS together.\n *\n *   [woocommerce_order_status_completed]\n *        │  condition: order total > 100  (only high-value orders pass)\n *        ▼\n *   Webhook A  \"Order Completed → CRM\"   ──chain──▶  Webhook B  \"High-Value Order → Fulfilment\"\n *   (synchronous, POST httpbin)                      (synchronous, POST httpbin)\n *\n * Webhook A maps the raw WC_Order into a tidy CRM payload and only fires when the\n * order total exceeds 100. On a successful 2xx delivery the chain forwards to\n * Webhook B, which receives A's sent payload + upstream response and maps a\n * compact fulfilment notification.\n *\n * The actual orders are placed in the next step (40-fire-woo-orders.php) — they\n * MUST be a later runPHP request so the webhook + trigger rows already exist in\n * the DB when WooCommerce's order-status hook fires (listeners attach on `init`).\n *\n * Idempotent: guarded by the chain name so long-lived previews don't duplicate.\n */\nrequire_once '/wordpress/wp-load.php';\n\nuse FlowSystems\\WebhookActions\\Repositories\\WebhookRepository;\nuse FlowSystems\\WebhookActions\\Repositories\\SchemaRepository;\nuse FlowSystems\\WebhookActions\\Repositories\\ChainRepository;\nuse FlowSystems\\WebhookActions\\Repositories\\ChainLinkRepository;\n\nif ( ! class_exists( ChainRepository::class ) || ! class_exists( WebhookRepository::class ) ) {\n    return;\n}\n\n// WooCommerce activation queues a redirect to its setup wizard on the next\n// admin load — drop it so the preview lands straight on the Webhooks screen.\ndelete_transient( '_wc_activation_redirect' );\n\n$chainRepo = new ChainRepository();\n\n// Idempotent: only seed on a fresh install.\nif ( $chainRepo->findByName( 'Order Fulfilment Pipeline' ) ) {\n    return;\n}\n\n$webhookRepo = new WebhookRepository();\n$schemaRepo  = new SchemaRepository();\n$linkRepo    = new ChainLinkRepository();\n\n// --- Webhook A: order completed → CRM (conditional, mapped) -----------------\n$sourceId = $webhookRepo->create([\n    'name'           => 'Order Completed → CRM',\n    'endpoint_url'   => 'https://httpbin.org/post',\n    'http_method'    => 'POST',\n    'is_synchronous' => 1,\n    'is_enabled'     => 1,\n    'triggers'       => ['woocommerce_order_status_completed'],\n]);\n\nif ( ! $sourceId ) {\n    return;\n}\n\n// WooCommerce passes ($order_id, $order); Dispatcher::normalizeValue() expands the\n// WC_Order via get_data(), so the order lives at args.1 with .total / .billing.* etc.\n// We leave example_payload empty — the first real order auto-captures the true\n// shape and SchemaRepository::upsert() preserves this mapping through that capture.\n$schemaRepo->upsert( (int) $sourceId, 'woocommerce_order_status_completed', [\n    'conditions' => [\n        'enabled' => true,\n        'type'    => 'and',\n        'rules'   => [\n            [\n                'field'    => 'args.1.total',\n                'operator' => 'greater_than',\n                'value'    => '100',\n                'cast'     => 'number',\n            ],\n        ],\n    ],\n    'conditions_evaluate_on' => 'original',\n    'field_mapping' => [\n        'mappings' => [\n            [ 'source' => 'args.1.id',                 'target' => 'order_id' ],\n            [ 'source' => 'args.1.status',             'target' => 'status' ],\n            [ 'source' => 'args.1.total',              'target' => 'order_total' ],\n            [ 'source' => 'args.1.currency',           'target' => 'currency' ],\n            [ 'source' => 'args.1.billing.email',      'target' => 'customer_email' ],\n            [ 'source' => 'args.1.billing.first_name', 'target' => 'customer_first_name' ],\n            [ 'source' => 'args.1.billing.last_name',  'target' => 'customer_last_name' ],\n        ],\n        'excluded'        => [],\n        'includeUnmapped' => false,\n    ],\n] );\n\n// --- Webhook B: chain target → fulfilment notification ----------------------\n$targetId = $webhookRepo->create([\n    'name'           => 'High-Value Order → Fulfilment',\n    'endpoint_url'   => 'https://httpbin.org/post',\n    'http_method'    => 'POST',\n    'is_synchronous' => 1,\n    'is_enabled'     => 1,\n    // No WP trigger — fired only via the chain link (synthetic trigger added below).\n    'triggers'       => [],\n]);\n\nif ( ! $targetId ) {\n    return;\n}\n\n// --- Wire the chain: A ──▶ B ------------------------------------------------\n$chainId = $chainRepo->create([\n    'name'        => 'Order Fulfilment Pipeline',\n    'description' => 'When a high-value order is sent to the CRM, notify fulfilment with the order + CRM response.',\n]);\n\nif ( ! $chainId ) {\n    return;\n}\n\n// create() also inserts the synthetic `fswa_chain_link:{linkId}` trigger row for B.\n$linkId = $linkRepo->create( (int) $chainId, (int) $sourceId, (int) $targetId );\n\nif ( $linkId ) {\n    // The chain hands B the full post-dispatch context as args[0]: A's sent\n    // (mapped) payload, the upstream response, and chain metadata. Map a compact\n    // fulfilment notification from it.\n    $schemaRepo->upsert( (int) $targetId, 'fswa_chain_link:' . $linkId, [\n        'field_mapping' => [\n            'mappings' => [\n                [ 'source' => 'args.0.payload.order_id',       'target' => 'order_id' ],\n                [ 'source' => 'args.0.payload.order_total',    'target' => 'order_total' ],\n                [ 'source' => 'args.0.payload.customer_email', 'target' => 'customer_email' ],\n                [ 'source' => 'args.0.source_webhook.name',    'target' => 'triggered_by' ],\n                [ 'source' => 'args.0.response.code',          'target' => 'crm_response_code' ],\n                [ 'source' => 'args.0.chain.depth',            'target' => 'chain_depth' ],\n            ],\n            'excluded'        => [],\n            'includeUnmapped' => false,\n        ],\n    ] );\n}"
    },
    {
      "step": "runPHP",
      "code": "<?php\n/**\n * Seed: place TWO real WooCommerce orders and mark them completed, firing\n * `woocommerce_order_status_completed` so the fulfilment pipeline (seed 30) runs\n * end to end and the Logs view is populated on landing.\n *\n *   Order #1  \"Premium Widget\"  $120.00  → total > 100  → condition PASSES\n *             → Webhook A delivers (success) → chain → Webhook B delivers (success)\n *   Order #2  \"Sticker Pack\"     $25.00  → total ≤ 100  → condition FAILS\n *             → Webhook A logged as SKIPPED, no delivery, no chain\n *\n * This MUST be a separate runPHP step (= separate request) from seed 30: the\n * trigger listener for `woocommerce_order_status_completed` is attached on `init`\n * from the stored webhook triggers, which only exist after seed 30 has run.\n *\n * Idempotent: guarded by an option flag so long-lived previews don't re-order.\n */\nrequire_once '/wordpress/wp-load.php';\n\nif ( ! function_exists( 'wc_create_order' ) || ! class_exists( 'WC_Product_Simple' ) ) {\n    return; // WooCommerce not active — nothing to do.\n}\n\nif ( get_option( 'fswa_demo_woo_orders_seeded' ) ) {\n    return;\n}\n\n/**\n * Create a published simple product and place a completed order for it.\n */\n$place_completed_order = static function ( string $name, string $price, array $billing ): void {\n    $product = new \\WC_Product_Simple();\n    $product->set_name( $name );\n    $product->set_regular_price( $price );\n    $product->set_price( $price );\n    $product->set_status( 'publish' );\n    $product->set_catalog_visibility( 'visible' );\n    $productId = $product->save();\n\n    $order = wc_create_order();\n    $order->add_product( wc_get_product( $productId ), 1 );\n    $order->set_address( $billing, 'billing' );\n    $order->calculate_totals();\n    $order->save();\n\n    // Transition to \"completed\" — fires woocommerce_order_status_completed($id, $order).\n    $order->update_status( 'completed', 'Live preview demo order.', true );\n};\n\n// Order #1 — high value, passes the > 100 condition (fires A → chain → B).\n$place_completed_order( 'Premium Widget', '120.00', [\n    'first_name' => 'Jane',\n    'last_name'  => 'Customer',\n    'email'      => 'jane.customer@example.com',\n    'country'    => 'US',\n    'city'       => 'Austin',\n    'state'      => 'TX',\n] );\n\n// Order #2 — low value, fails the condition (Webhook A is logged as skipped).\n$place_completed_order( 'Sticker Pack', '25.00', [\n    'first_name' => 'Sam',\n    'last_name'  => 'Shopper',\n    'email'      => 'sam.shopper@example.com',\n    'country'    => 'US',\n    'city'       => 'Denver',\n    'state'      => 'CO',\n] );\n\nupdate_option( 'fswa_demo_woo_orders_seeded', 1, false );"
    }
  ]
}
