[
  {
    "path": ".editorconfig",
    "content": "# This file is for unifying the coding style for different editors and IDEs\n# editorconfig.org\n\n# WordPress Coding Standards\n# https://make.wordpress.org/core/handbook/coding-standards/\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\ntab_width = 4\nindent_style = tab\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.txt]\ntrim_trailing_whitespace = false\n\n[*.{md,json,yml}]\ntrim_trailing_whitespace = false\nindent_style = space\nindent_size = 2"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing ✨\n\nYour help will be greatly appreciated :)\n\nWooCommerce is licensed under the GPLv3+, and all contributions to the project will be released under the same license. You maintain copyright over any contribution you make, and by submitting a pull request, you are agreeing to release that contribution under the GPLv3+ license.\n\n## Coding Guidelines and Development 🛠\n\n- Ensure you stick to the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/)\n- Whenever possible please fix pre-existing code standards errors in the files that you change. It is ok to skip that for larger files or complex fixes.\n- Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured.\n- When committing, reference your issue number (#1234) and include a note about the fix.\n- Push the changes to your fork and submit a pull request on the trunk branch of the repository.\n- Make sure to write good and detailed commit messages (see [this post](https://chris.beams.io/posts/git-commit/) for more on this) and follow all the applicable sections of the pull request template.\n- Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the team.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1-bug-report.yml",
    "content": "name: 🐞 Bug Report\ndescription: Report a bug if something isn't working as expected in WooCommerce Smooth Generator.\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### Thanks for contributing!\n\n        Please provide us with the information requested in this bug report. \n        Without these details, we won't be able to fully evaluate this issue. \n        Bug reports lacking detail, or for any other reason than to report a bug, may be closed without action.\n\n        Make sure to look through the [existing `bug` issues](https://github.com/woocommerce/wc-smooth-generator/issues?q=is%3Aopen+is%3Aissue+label%3Abug) to see whether your bug has already been submitted.\n        Feel free to contribute to any existing issues.\n  - type: checkboxes\n    id: prerequisites\n    attributes:\n      label: Prerequisites\n      description: Please confirm these before submitting the issue.\n      options:\n        - label: I have carried out troubleshooting steps and I believe I have found a bug.\n        - label: I have searched for similar bugs in both open and closed issues and cannot find a duplicate.\n    validations:\n      required: true\n  - type: textarea\n    id: summary\n    attributes:\n      label: Describe the bug\n      description: |\n        A clear and concise description of what the bug is and what actually happens. Please be as descriptive as possible.\n        Please also include any error logs or output.\n        If applicable you can attach screenshot(s) or recording(s) directly by dragging & dropping.\n    validations:\n      required: true\n  - type: textarea\n    id: environment\n    attributes:\n      label: WordPress Environment\n      description: |\n        Please share the [WooCommerce System Status Report](https://woocommerce.com/document/understanding-the-woocommerce-system-status-report/) of your site to help us evaluate the issue. \n      placeholder: |\n        The System Status Report is found in your WordPress admin under **WooCommerce > Status**. \n        Please select “Get system report”, then “Copy for support”, and then paste it here.\n  - type: checkboxes\n    id: isolating\n    attributes:\n      label: Isolating the problem\n      description: |\n        Please try testing your site for theme and plugins conflict. \n        To do that deactivate all plugins except for WooCommerce and WooCommerce Smooth Generator and switch to a default WordPress theme or [Storefront](https://en-gb.wordpress.org/themes/storefront/). Then test again. \n        If the issue is resolved with the default theme and all plugins deactivated, it means that one of your plugins or a theme is causing the issue. \n        You will then need to enable it one by one and test every time you do that in order to figure out which plugin is causing the issue.\n      options:\n        - label: I have deactivated other plugins and confirmed this bug occurs when only WooCommerce and WooCommerce Smooth Generator plugins are active.\n        - label: This bug happens with a default WordPress theme active, or [Storefront](https://woocommerce.com/storefront/).\n        - label: I can reproduce this bug consistently using the steps above.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2-enhancement.yml",
    "content": "name: ✨ Enhancement Request\ndescription: If you have an idea to improve WooCommerce Smooth Generator or need something for development please let us know or submit a Pull Request!\ntitle: \"[Enhancement]: \"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### Thanks for contributing!\n\n        Please provide us with the information requested in this form. \n\n        Make sure to look through [existing `enhancement` issues](https://github.com/woocommerce/wc-smooth-generator/issues?q=is%3Aissue+label%3Aenhancement+is%3Aopen) to see whether your idea is already being discussed.\n        Feel free to contribute to any existing issues.\n  - type: textarea\n    id: summary\n    attributes:\n        label: Describe the solution you'd like\n        description: A clear and concise description of what you want to happen.\n    validations:\n      required: true\n  - type: textarea\n    id: alternative\n    attributes:\n      label: Describe alternatives you've considered\n      description: A clear and concise description of any alternative solutions or features you've considered.\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: 🔒 Security issue\n    url: https://hackerone.com/automattic/\n    about: For security reasons, please report all security issues via HackerOne. Also, if the issue is valid, a bug bounty will be paid out to you. Please disclose responsibly and not via GitHub (which allows for exploiting issues in the wild before the patch is released).\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!-- This form is for other issue types specific to WooCommerce Smooth Generator. This is not a support portal. -->\n\n**Prerequisites (mark completed items with an [x]):**\n- [ ] I have checked that my issue type is not listed here https://github.com/woocommerce/wc-smooth-generator/issues/new/choose\n- [ ] My issue is not a security issue, bug report, or enhancement (Please use the link above if it is).\n\n**Issue Description:**\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "### All Submissions:\n\n* [ ] Have you followed the [Contributing guidelines](https://github.com/woocommerce/wc-smooth-generator/blob/trunk/.github/CONTRIBUTING.md)?\n* [ ] Does your code follow the [WordPress' coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/)?\n* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/woocommerce/wc-smooth-generator/pulls) for the same update/change?\n\n<!-- Mark completed items with an [x] -->\n\n<!-- You can erase any parts of this template not applicable to your Pull Request. -->\n\n### Changes proposed in this Pull Request:\n\n<!-- Describe the changes made to this Pull Request and the reason for such changes. -->\n\nCloses # .\n\n### How to test the changes in this Pull Request:\n\n1.\n2.\n3.\n\n### Other information:\n\n* [ ] Have you added an explanation of what your changes do and why you'd like us to include them?\n* [ ] Have you written new tests for your changes, as applicable?\n* [ ] Have you successfully run tests with your changes locally?\n\n<!-- Mark completed items with an [x] -->\n\n### Changelog entry\n\n> Enter a summary of all changes on this Pull Request. This will appear in the changelog if accepted.\n\n### FOR PR REVIEWER ONLY:\n\n* [ ] I have reviewed that everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities. I made sure Linting is not ignored or disabled.\n"
  },
  {
    "path": ".github/workflows/php-unit-tests.yml",
    "content": "name: PHP Unit Tests\n\non:\n    push:\n        branches:\n            - trunk\n        paths:\n            - '**.php'\n            - composer.json\n            - composer.lock\n            - phpunit.xml.dist\n            - tests/**\n            - .github/workflows/php-unit-tests.yml\n    pull_request:\n        paths:\n            - '**.php'\n            - composer.json\n            - composer.lock\n            - phpunit.xml.dist\n            - tests/**\n            - .github/workflows/php-unit-tests.yml\n    workflow_dispatch:\n\nconcurrency:\n    group: ${{ github.workflow }}-${{ github.ref }}\n    cancel-in-progress: true\n\njobs:\n    UnitTests:\n        name: PHP unit tests - PHP ${{ matrix.php }}, WP ${{ matrix.wp-version }}\n        runs-on: ubuntu-latest\n        env:\n            WP_CORE_DIR: '/tmp/wordpress/src'\n            WP_TESTS_DIR: '/tmp/wordpress/tests/phpunit'\n        strategy:\n            matrix:\n                php: ['7.4', '8.2', '8.5']\n                wp-version: ['latest']\n\n        services:\n            mysql:\n                image: mysql:8.0\n                env:\n                    MYSQL_ROOT_PASSWORD: root\n                    MYSQL_DATABASE: wordpress_test\n                ports:\n                    - 3306:3306\n                options: --health-cmd=\"mysqladmin ping\" --health-interval=10s --health-timeout=5s --health-retries=3\n\n        steps:\n            - name: Checkout repository\n              uses: actions/checkout@v4\n\n            - name: Setup PHP\n              uses: shivammathur/setup-php@v2\n              with:\n                  php-version: ${{ matrix.php }}\n                  extensions: mbstring, intl, mysql\n                  coverage: none\n                  tools: composer\n\n            - name: Install Composer dependencies\n              run: composer install --prefer-dist --no-progress\n\n            - name: Install SVN (used for installing WP versions)\n              run: sudo apt-get update && sudo apt-get install -y subversion\n\n            - name: Install WP tests\n              run: echo \"y\" | bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wp-version }}\n\n            - name: Install WooCommerce\n              run: |\n                  mkdir -p /tmp/wordpress/src/wp-content/plugins\n                  cd /tmp/wordpress/src/wp-content/plugins\n                  wget https://downloads.wordpress.org/plugin/woocommerce.latest-stable.zip\n                  unzip -q woocommerce.latest-stable.zip\n                  rm woocommerce.latest-stable.zip\n\n            - name: Run PHP unit tests\n              run: composer test-unit\n\n    PHPCS:\n        name: PHPCS\n        runs-on: ubuntu-latest\n        if: github.event_name == 'pull_request'\n        steps:\n            - name: Checkout repository\n              uses: actions/checkout@v4\n              with:\n                  fetch-depth: 0\n\n            - name: Setup PHP\n              uses: shivammathur/setup-php@v2\n              with:\n                  php-version: '8.2'\n                  tools: composer\n\n            - name: Install Composer dependencies\n              run: composer install --prefer-dist --no-progress\n\n            - name: Get Changed Files\n              id: changed-files\n              uses: tj-actions/changed-files@v41\n              with:\n                  files: '**/*.php'\n\n            - name: Run PHPCS on changed files\n              if: steps.changed-files.outputs.any_changed == 'true'\n              run: vendor/bin/phpcs-changed -s --git --git-base ${{ github.event.pull_request.base.sha }} ${{ steps.changed-files.outputs.all_changed_files }}\n"
  },
  {
    "path": ".gitignore",
    "content": "vendor/\nnode_modules/\nwc-smooth-generator.zip\nwc-smooth-generator/\n\n# PHPStorm\n.idea\n\n# PHPCS\n.phpcs.xml\nphpcs.xml\n\n# PHPUnit\n.phpunit.result.cache\n\n# PHP xdebug\n.vscode/launch.json"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\ncomposer run lint-staged\n"
  },
  {
    "path": ".nvmrc",
    "content": "v22\n"
  },
  {
    "path": "README.md",
    "content": "# WooCommerce Smooth Generator\n\nGenerate realistic WooCommerce products, orders, customers, coupons, and taxonomy terms for development, testing, and demos.\n\nWP-CLI is the primary interface. A limited WP Admin UI is also available at Dashboard > Tools > WooCommerce Smooth Generator.\n\n## Installation\n\n**From GitHub releases (recommended):**\n\n1. Download the latest zip from [GitHub Releases](https://github.com/woocommerce/wc-smooth-generator/releases/).\n2. Install via WP Admin > Plugins > Add New > Upload Plugin.\n\n**From source:**\n\n```bash\ngit clone https://github.com/woocommerce/wc-smooth-generator.git\ncd wc-smooth-generator\ncomposer install --no-dev\n```\n\n## Requirements\n\n- PHP 7.4+\n- WordPress (tested up to 6.9)\n- WooCommerce 5.0+\n\n## WP-CLI commands\n\nAll commands use the `wp wc generate` prefix. Run `wp help wc generate` for a summary, or `wp help wc generate <command>` for detailed usage.\n\n### Products\n\n```bash\n# Generate 10 products (default, mix of simple and variable)\nwp wc generate products\n\n# Generate 25 simple products\nwp wc generate products 25 --type=simple\n\n# Generate variable products using only existing categories and tags\nwp wc generate products 10 --type=variable --use-existing-terms\n```\n\n| Option | Description |\n|---|---|\n| `<amount>` | Number of products to generate. Default: `10` |\n| `--type=<type>` | Product type: `simple` or `variable`. Default: random mix |\n| `--use-existing-terms` | Only use existing categories and tags instead of generating new ones |\n\n### Orders\n\n```bash\n# Generate 10 orders for today's date\nwp wc generate orders\n\n# Generate orders with random dates in a range\nwp wc generate orders 50 --date-start=2024-01-01 --date-end=2024-12-31\n\n# Generate completed orders with a specific status\nwp wc generate orders 20 --status=completed\n\n# Apply coupons to half the orders\nwp wc generate orders 100 --coupon-ratio=0.5\n\n# Refund 30% of completed orders\nwp wc generate orders 50 --status=completed --refund-ratio=0.3\n```\n\n| Option | Description |\n|---|---|\n| `<amount>` | Number of orders to generate. Default: `10` |\n| `--date-start=<date>` | Earliest order date (YYYY-MM-DD). Dates are randomized between this and today or `--date-end` |\n| `--date-end=<date>` | Latest order date (YYYY-MM-DD). Requires `--date-start` |\n| `--status=<status>` | Order status: `completed`, `processing`, `on-hold`, or `failed`. Default: random mix |\n| `--coupons` | Apply a coupon to every order. Equivalent to `--coupon-ratio=1.0` |\n| `--coupon-ratio=<ratio>` | Fraction of orders that get coupons (0.0-1.0). Creates 6 coupons if none exist (3 fixed cart, 3 percentage) |\n| `--refund-ratio=<ratio>` | Fraction of completed orders to refund (0.0-1.0). In batch mode: 50% full, 25% partial, 25% multi-partial. Single-order mode uses probabilistic distribution |\n| `--skip-order-attribution` | Skip generating order attribution metadata |\n\n**Batch distribution:** When generating multiple orders, coupon and refund counts are deterministic (selection without replacement). For odd totals, `round()` distributes coupons and remainders go to multi-partial refunds. Single-order generation uses probabilistic distribution.\n\n**Order attribution:** Random attribution metadata (device type, UTM parameters, referrer, session data) is added by default. Orders dated before 2024-01-09 skip attribution, since the feature didn't exist in WooCommerce yet.\n\n### Customers\n\n```bash\n# Generate 10 customers (70% people, 30% companies)\nwp wc generate customers\n\n# Generate Spanish company customers\nwp wc generate customers 20 --country=ES --type=company\n```\n\n| Option | Description |\n|---|---|\n| `<amount>` | Number of customers to generate. Default: `10` |\n| `--country=<code>` | ISO 3166-1 alpha-2 country code (e.g., `US`, `ES`, `CN`). Localizes names and addresses. Default: random from store selling locations |\n| `--type=<type>` | Customer type: `person` or `company`. Default: 70/30 mix |\n\n### Coupons\n\n```bash\n# Generate 10 coupons with default discount range (5-100)\nwp wc generate coupons\n\n# Generate percentage coupons between 5% and 25%\nwp wc generate coupons 20 --discount_type=percent --min=5 --max=25\n```\n\n| Option | Description |\n|---|---|\n| `<amount>` | Number of coupons to generate. Default: `10` |\n| `--min=<amount>` | Minimum discount amount. Default: `5` |\n| `--max=<amount>` | Maximum discount amount. Default: `100` |\n| `--discount_type=<type>` | Discount type: `fixed_cart` or `percent`. Default: `fixed_cart` |\n\n### Terms\n\n```bash\n# Generate 10 product tags\nwp wc generate terms product_tag 10\n\n# Generate hierarchical product categories\nwp wc generate terms product_cat 50 --max-depth=3\n\n# Generate child categories under an existing category\nwp wc generate terms product_cat 10 --parent=123\n```\n\n| Option | Description |\n|---|---|\n| `<taxonomy>` | Required. Taxonomy to generate terms for: `product_cat` or `product_tag` |\n| `<amount>` | Number of terms to generate. Default: `10` |\n| `--max-depth=<levels>` | Maximum hierarchy depth (1-5). Only applies to `product_cat`. Default: `1` (flat) |\n| `--parent=<term_id>` | Create all terms as children of this existing term ID. Only applies to `product_cat` |\n\n## Programmatic usage\n\nAll generators live in the `WC\\SmoothGenerator\\Generator` namespace and expose `generate()` and `batch()` static methods.\n\n### Single objects\n\n```php\nuse WC\\SmoothGenerator\\Generator;\n\n// Generate and save a product (returns WC_Product or WP_Error).\n$product = Generator\\Product::generate( true, [ 'type' => 'simple' ] );\n\n// Generate and save an order (returns WC_Order or false).\n$order = Generator\\Order::generate( true, [ 'status' => 'completed' ] );\n\n// Generate and save a customer (returns WC_Customer or WP_Error).\n$customer = Generator\\Customer::generate( true, [ 'country' => 'US', 'type' => 'person' ] );\n\n// Generate and save a coupon (returns WC_Coupon or WP_Error).\n$coupon = Generator\\Coupon::generate( true, [ 'min' => 5, 'max' => 25, 'discount_type' => 'percent' ] );\n\n// Generate and save a term (returns WP_Term or WP_Error).\n$term = Generator\\Term::generate( true, 'product_cat', 0 );\n```\n\n### Batch generation\n\n```php\nuse WC\\SmoothGenerator\\Generator;\n\n// Generate 50 products (returns array of product IDs or WP_Error).\n// Max batch size: 100.\n$product_ids = Generator\\Product::batch( 50, [ 'type' => 'variable', 'use-existing-terms' => true ] );\n\n// Generate 100 orders with date range and coupons.\n$order_ids = Generator\\Order::batch( 100, [\n    'date-start'   => '2024-01-01',\n    'date-end'     => '2024-06-30',\n    'status'       => 'completed',\n    'coupon-ratio' => '0.3',\n    'refund-ratio' => '0.2',\n] );\n\n// Generate 25 customers.\n$customer_ids = Generator\\Customer::batch( 25, [ 'country' => 'ES' ] );\n\n// Generate 10 coupons.\n$coupon_ids = Generator\\Coupon::batch( 10, [ 'min' => 1, 'max' => 50 ] );\n\n// Generate 20 hierarchical product categories.\n$term_ids = Generator\\Term::batch( 20, 'product_cat', [ 'max-depth' => 3 ] );\n```\n\n### Action hooks\n\nEach generator fires an action after creating an object:\n\n- `smoothgenerator_product_generated` -- after a product is saved\n- `smoothgenerator_order_generated` -- after an order is saved\n- `smoothgenerator_customer_generated` -- after a customer is saved\n- `smoothgenerator_coupon_generated` -- after a coupon is saved\n- `smoothgenerator_term_generated` -- after a term is saved\n\n## Available generators\n\n### Product generator\n\nCreates simple or variable products with:\n\n- Name, SKU, global unique ID, featured status\n- Price, sale price, sale date scheduling\n- Tax status and class, stock management\n- Product image and gallery images (auto-generated)\n- Categories, tags, and brands (if the `product_brand` taxonomy exists)\n- Upsells and cross-sells from existing products\n- Attributes and variations (for variable products)\n- Virtual/downloadable flags, dimensions, weight\n- Cost of Goods Sold (if WooCommerce COGS is enabled)\n- Reviews allowed toggle, purchase notes, menu order\n\n### Order generator\n\nCreates orders with realistic data:\n\n- Billing and shipping addresses from the customer\n- Line items from existing products\n- Random status distribution (or a specific status)\n- Date randomization within a given range\n- Coupon application with configurable ratio\n- Refunds: full, partial, and multi-partial with realistic timing\n- Order attribution: device type, UTM parameters, referrer, session data\n- Extra fees (~20% chance per order)\n- Paid and completed dates based on status\n\n### Customer generator\n\nCreates customer accounts with localized data:\n\n- Person (first/last name) or company profiles\n- Localized names, emails, and phone numbers based on country\n- Billing address with street, city, state, postcode\n- Shipping address (50% chance; half copy billing, half are unique)\n- Username and password\n\n### Coupon generator\n\nCreates discount coupons:\n\n- Auto-generated coupon codes\n- Configurable discount range (min/max)\n- Fixed cart or percentage discount type\n\n### Term generator\n\nCreates taxonomy terms for products:\n\n- Product categories (`product_cat`) with optional hierarchy (up to 5 levels deep)\n- Product tags (`product_tag`)\n- Auto-generated descriptions\n- Child terms under a specified parent\n\n## Contributing\n\nFound a bug or want a feature? [Open an issue](https://github.com/woocommerce/wc-smooth-generator/issues) or submit a pull request.\n\n### Development setup\n\nRequires Node.js v16 and Composer v2+.\n\n```bash\nnpm run setup\n```\n\nThis installs dependencies and sets up a pre-commit hook that lints PHP changes using the WooCommerce Core phpcs ruleset.\n\n## License\n\n[GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)\n"
  },
  {
    "path": "TESTING.md",
    "content": "# Testing Guide for Exact Ratio Distribution\n\nThis document provides comprehensive test cases for verifying the exact ratio distribution feature for coupons and refunds in batch mode.\n\n## Prerequisites\n\n- WordPress installation with WooCommerce\n- WC Smooth Generator plugin installed\n- WP-CLI access\n- Some products already generated (run `wp wc generate products 50` if needed)\n\n## Test Cases\n\n### 1. Basic Coupon Ratio Tests\n\n#### Test 1.1: Exact 50% coupon ratio\n```bash\nwp wc generate orders 100 --coupon-ratio=0.5\n```\n**Expected Result:** Exactly 50 orders with coupons\n\n**Verification:**\n```bash\n# Count orders with coupons via database\nwp db query \"SELECT COUNT(DISTINCT order_id) FROM wp_woocommerce_order_items WHERE order_item_type = 'coupon'\"\n```\n\n#### Test 1.2: Edge case - 0.0 ratio (no coupons)\n```bash\nwp wc generate orders 50 --coupon-ratio=0.0\n```\n**Expected Result:** 0 orders with coupons\n\n#### Test 1.3: Edge case - 1.0 ratio (all coupons)\n```bash\nwp wc generate orders 50 --coupon-ratio=1.0\n```\n**Expected Result:** Exactly 50 orders with coupons\n\n#### Test 1.4: Odd number rounding\n```bash\nwp wc generate orders 11 --coupon-ratio=0.5\n```\n**Expected Result:** Exactly 6 orders with coupons (5.5 rounds to 6)\n\n### 2. Basic Refund Ratio Tests\n\n#### Test 2.1: Exact 40% refund ratio with distribution\n```bash\nwp wc generate orders 100 --status=completed --refund-ratio=0.4\n```\n**Expected Result:**\n- Total: 40 refunds\n- Distribution: ~20 full, ~10 single partial, ~10 multi-partial\n\n**Verification:**\n```bash\n# Count total refunds\nwp db query \"SELECT COUNT(*) FROM wp_posts WHERE post_type = 'shop_order_refund'\"\n\n# Count full refunds (orders with status 'refunded')\nwp db query \"SELECT COUNT(*) FROM wp_posts WHERE post_type = 'shop_order' AND post_status = 'wc-refunded'\"\n```\n\n#### Test 2.2: Edge case - 0.0 ratio (no refunds)\n```bash\nwp wc generate orders 50 --status=completed --refund-ratio=0.0\n```\n**Expected Result:** 0 refunds\n\n#### Test 2.3: Edge case - 1.0 ratio (all refunds)\n```bash\nwp wc generate orders 50 --status=completed --refund-ratio=1.0\n```\n**Expected Result:** Exactly 50 refunds (distributed 50/25/25)\n\n#### Test 2.4: Odd number rounding for refunds\n```bash\nwp wc generate orders 11 --status=completed --refund-ratio=0.4\n```\n**Expected Result:**\n- Total: 4 refunds (rounded)\n- Distribution: ~2 full, ~1 partial, ~1 multi (remainder)\n\n### 3. Parameter Precedence Tests\n\n#### Test 3.1: Legacy --coupons flag\n```bash\nwp wc generate orders 20 --coupons\n```\n**Expected Result:** All 20 orders have coupons (legacy behavior preserved)\n\n#### Test 3.2: Coupon ratio without legacy flag\n```bash\nwp wc generate orders 100 --coupon-ratio=0.3\n```\n**Expected Result:** Exactly 30 orders with coupons\n\n#### Test 3.3: Both flags (ratio should be ignored when legacy flag present)\n```bash\nwp wc generate orders 100 --coupons --coupon-ratio=0.3\n```\n**Expected Result:** All 100 orders have coupons (--coupons takes precedence)\n\n### 4. Single Order Generation (Probabilistic Fallback)\n\n#### Test 4.1: Single order with coupon ratio should use probabilistic\n```bash\n# Run multiple times to verify probabilistic behavior\nwp wc generate orders 1 --coupon-ratio=0.5\nwp wc generate orders 1 --coupon-ratio=0.5\nwp wc generate orders 1 --coupon-ratio=0.5\n```\n**Expected Result:** Approximately 50% of single orders will have coupons (varies each run)\n\n### 5. Refund Distribution Verification\n\n#### Test 5.1: Verify 50/25/25 refund split\n```bash\n# Generate orders and check distribution\nwp wc generate orders 200 --status=completed --refund-ratio=0.5\n```\n**Expected Result:**\n- Total refunds: 100\n- Full refunds (~50): Orders with status \"refunded\"\n- Single partial (~25): Orders with 1 refund, status still \"completed\"\n- Multi-partial (~25): Orders with 2 refunds, status still \"completed\"\n\n**Manual Verification:**\n1. Check a sample of fully refunded orders in WP Admin\n2. Check a sample of partially refunded orders\n3. Count number of refund entries per order\n\n### 6. Combined Parameters\n\n#### Test 6.1: Date range + coupon ratio + refund ratio\n```bash\nwp wc generate orders 100 --date-start=2024-01-01 --date-end=2024-12-31 --status=completed --coupon-ratio=0.4 --refund-ratio=0.3\n```\n**Expected Result:**\n- Orders spread across date range\n- Exactly 40 orders with coupons\n- Exactly 30 orders with refunds (distributed 50/25/25)\n\n### 7. Failed Orders Edge Case\n\n#### Test 7.1: Verify failed orders don't affect count\n```bash\n# If products are missing or invalid, some orders may fail\nwp wc generate orders 100 --coupon-ratio=0.5\n```\n**Expected Result:**\n- Count only successful orders\n- If only 95 orders succeed, there should be exactly 47-48 with coupons (based on 95, not 100)\n\n## Verification Queries\n\n### Count Orders with Coupons\n```bash\nwp db query \"SELECT COUNT(DISTINCT order_id) FROM wp_woocommerce_order_items WHERE order_item_type = 'coupon'\"\n```\n\n### Count All Refunds\n```bash\nwp db query \"SELECT COUNT(*) FROM wp_posts WHERE post_type = 'shop_order_refund'\"\n```\n\n### Count Full Refunds (Orders with 'refunded' status)\n```bash\nwp db query \"SELECT COUNT(*) FROM wp_posts WHERE post_type = 'shop_order' AND post_status = 'wc-refunded'\"\n```\n\n### Count Partial Refunds\n```bash\nwp db query \"\nSELECT COUNT(*) as partial_refund_orders\nFROM (\n    SELECT p.ID, COUNT(r.ID) as refund_count\n    FROM wp_posts p\n    LEFT JOIN wp_posts r ON r.post_parent = p.ID AND r.post_type = 'shop_order_refund'\n    WHERE p.post_type = 'shop_order' AND p.post_status = 'wc-completed'\n    GROUP BY p.ID\n    HAVING refund_count > 0\n) as refunded_completed\n\"\n```\n\n### Count Multi-Partial Refunds (2 refunds on same order)\n```bash\nwp db query \"\nSELECT COUNT(*) as multi_partial_orders\nFROM (\n    SELECT p.ID, COUNT(r.ID) as refund_count\n    FROM wp_posts p\n    LEFT JOIN wp_posts r ON r.post_parent = p.ID AND r.post_type = 'shop_order_refund'\n    WHERE p.post_type = 'shop_order'\n    GROUP BY p.ID\n    HAVING refund_count = 2\n) as multi_refunded\n\"\n```\n\n## Notes\n\n- All exact ratio tests assume successful order generation\n- Exact ratio distribution uses O(1) memory via dynamic counters (selection without replacement algorithm)\n- Works for any batch size without memory constraints\n- Ratios are rounded using PHP's `round()` function for odd numbers\n"
  },
  {
    "path": "bin/install-wp-tests.sh",
    "content": "#!/usr/bin/env bash\n\nif [ $# -lt 3 ]; then\n\techo \"usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]\"\n\texit 1\nfi\n\nDB_NAME=$1\nDB_USER=$2\nDB_PASS=$3\nDB_HOST=${4-localhost}\nWP_VERSION=${5-latest}\nSKIP_DB_CREATE=${6-false}\n\nTMPDIR=${TMPDIR-/tmp}\nTMPDIR=$(echo $TMPDIR | sed -e \"s/\\/$//\")\nWP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}\nWP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}\n\ndownload() {\n    if [ `which curl` ]; then\n        curl -s \"$1\" > \"$2\";\n    elif [ `which wget` ]; then\n        wget -nv -O \"$2\" \"$1\"\n    fi\n}\n\nif [[ $WP_VERSION =~ ^[0-9]+\\.[0-9]+\\-(beta|RC)[0-9]+$ ]]; then\n\tWP_BRANCH=${WP_VERSION%\\-*}\n\tWP_TESTS_TAG=\"branches/$WP_BRANCH\"\n\nelif [[ $WP_VERSION =~ ^[0-9]+\\.[0-9]+$ ]]; then\n\tWP_TESTS_TAG=\"branches/$WP_VERSION\"\nelif [[ $WP_VERSION =~ [0-9]+\\.[0-9]+\\.[0-9]+ ]]; then\n\tif [[ $WP_VERSION =~ [0-9]+\\.[0-9]+\\.[0] ]]; then\n\t\t# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x\n\t\tWP_TESTS_TAG=\"tags/${WP_VERSION%??}\"\n\telse\n\t\tWP_TESTS_TAG=\"tags/$WP_VERSION\"\n\tfi\nelif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then\n\tWP_TESTS_TAG=\"trunk\"\nelse\n\t# http serves a single offer, whereas https serves multiple. we only want one\n\tdownload http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json\n\tgrep '[0-9]+\\.[0-9]+(\\.[0-9]+)?' /tmp/wp-latest.json\n\tLATEST_VERSION=$(grep -o '\"version\":\"[^\"]*' /tmp/wp-latest.json | sed 's/\"version\":\"//')\n\tif [[ -z \"$LATEST_VERSION\" ]]; then\n\t\techo \"Latest WordPress version could not be found\"\n\t\texit 1\n\tfi\n\tWP_TESTS_TAG=\"tags/$LATEST_VERSION\"\nfi\nset -ex\n\ninstall_wp() {\n\n\tif [ -d $WP_CORE_DIR ]; then\n\t\treturn;\n\tfi\n\n\tmkdir -p $WP_CORE_DIR\n\n\tif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then\n\t\tmkdir -p $TMPDIR/wordpress-trunk\n\t\trm -rf $TMPDIR/wordpress-trunk/*\n\t\tsvn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress\n\t\tmv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR\n\telse\n\t\tif [ $WP_VERSION == 'latest' ]; then\n\t\t\tlocal ARCHIVE_NAME='latest'\n\t\telif [[ $WP_VERSION =~ [0-9]+\\.[0-9]+ ]]; then\n\t\t\t# https serves multiple offers, whereas http serves single.\n\t\t\tdownload https://wordpress.org/wordpress-$WP_VERSION.tar.gz  $TMPDIR/wordpress.tar.gz\n\t\t\tARCHIVE_NAME=\"wordpress-$WP_VERSION\"\n\t\tfi\n\n\t\tif [ ! -z \"$ARCHIVE_NAME\" ]; then\n\t\t\tdownload https://wordpress.org/${ARCHIVE_NAME}.tar.gz  $TMPDIR/wordpress.tar.gz\n\t\t\ttar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR\n\t\tfi\n\tfi\n\n\tdownload https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php\n}\n\ninstall_test_suite() {\n\t# portable in-place argument for both GNU sed and Mac OSX sed\n\tif [[ $(uname -s) == 'Darwin' ]]; then\n\t\tlocal ioption='-i.bak'\n\telse\n\t\tlocal ioption='-i'\n\tfi\n\n\t# set up testing suite if it doesn't yet exist\n\tif [ ! -d $WP_TESTS_DIR ]; then\n\t\t# set up testing suite\n\t\tmkdir -p $WP_TESTS_DIR\n\t\trm -rf $WP_TESTS_DIR/{includes,data}\n\t\tsvn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes\n\t\tsvn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data\n\tfi\n\n\tif [ ! -f wp-tests-config.php ]; then\n\t\tdownload https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php \"$WP_TESTS_DIR\"/wp-tests-config.php\n\t\t# remove all forward slashes in the end\n\t\tWP_CORE_DIR=$(echo $WP_CORE_DIR | sed \"s:/\\+$::\")\n\t\tsed $ioption \"s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':\" \"$WP_TESTS_DIR\"/wp-tests-config.php\n\t\tsed $ioption \"s/youremptytestdbnamehere/$DB_NAME/\" \"$WP_TESTS_DIR\"/wp-tests-config.php\n\t\tsed $ioption \"s/yourusernamehere/$DB_USER/\" \"$WP_TESTS_DIR\"/wp-tests-config.php\n\t\tsed $ioption \"s/yourpasswordhere/$DB_PASS/\" \"$WP_TESTS_DIR\"/wp-tests-config.php\n\t\tsed $ioption \"s|localhost|${DB_HOST}|\" \"$WP_TESTS_DIR\"/wp-tests-config.php\n\tfi\n\n}\n\nrecreate_db() {\n\tshopt -s nocasematch\n\tif [[ $1 =~ ^(y|yes)$ ]]\n\tthen\n\t\tmysqladmin drop $DB_NAME -f --user=\"$DB_USER\" --password=\"$DB_PASS\"$EXTRA\n\t\tcreate_db\n\t\techo \"Recreated the database ($DB_NAME).\"\n\telse\n\t\techo \"Leaving the existing database ($DB_NAME) in place.\"\n\tfi\n\tshopt -u nocasematch\n}\n\ncreate_db() {\n\tmysqladmin create $DB_NAME --user=\"$DB_USER\" --password=\"$DB_PASS\"$EXTRA\n}\n\ninstall_db() {\n\n\tif [ ${SKIP_DB_CREATE} = \"true\" ]; then\n\t\treturn 0\n\tfi\n\n\t# parse DB_HOST for port or socket references\n\tlocal PARTS=(${DB_HOST//\\:/ })\n\tlocal DB_HOSTNAME=${PARTS[0]};\n\tlocal DB_SOCK_OR_PORT=${PARTS[1]};\n\tlocal EXTRA=\"\"\n\n\tif ! [ -z $DB_HOSTNAME ] ; then\n\t\tif [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\\{1,\\}$') ]; then\n\t\t\tEXTRA=\" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp\"\n\t\telif ! [ -z $DB_SOCK_OR_PORT ] ; then\n\t\t\tEXTRA=\" --socket=$DB_SOCK_OR_PORT\"\n\t\telif ! [ -z $DB_HOSTNAME ] ; then\n\t\t\tEXTRA=\" --host=$DB_HOSTNAME --protocol=tcp\"\n\t\tfi\n\tfi\n\n\t# create database\n\tif [ $(mysql --user=\"$DB_USER\" --password=\"$DB_PASS\"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ]\n\tthen\n\t\techo \"Reinstalling will delete the existing test database ($DB_NAME)\"\n\t\tread -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB\n\t\trecreate_db $DELETE_EXISTING_DB\n\telse\n\t\tcreate_db\n\tfi\n}\n\ninstall_wp\ninstall_test_suite\ninstall_db\n"
  },
  {
    "path": "bin/lint-branch.sh",
    "content": "#!/bin/bash\n\n# Lint branch\n#\n# Runs phpcs-changed, comparing the current branch to its \"base\" or \"parent\" branch.\n# The base branch defaults to trunk, but another branch name can be specified as an\n# optional positional argument.\n#\n# Example:\n# ./lint-branch.sh base-branch\n\nbaseBranch=${1:-\"trunk\"}\n\nchangedFiles=$(git diff $(git merge-base HEAD $baseBranch) --relative --name-only -- '*.php')\n\n# Only complete this if changed files are detected.\n[[ -z $changedFiles ]] || composer exec phpcs-changed -- -s --git --git-base $baseBranch $changedFiles\n"
  },
  {
    "path": "changelog.txt",
    "content": "*** Changelog ***\n\n2026-02-19 - version 1.3.0\n* Add - Brand support for generated products (product_brand taxonomy).\n* Add - Comprehensive unit test suite with PHPUnit tests.\n* Add - GitHub Actions workflow for automated PHP unit testing across multiple PHP versions.\n* Add - Exact ratio distribution for deterministic test data generation with --coupon-ratio and --refund-ratio flags.\n* Add - Cost of goods sold (COGS) support for generated products.\n* Add - Global unique ID (_wc_gla_gtin) support for product generation.\n* Add - Additional referral sources to order attribution data.\n* Add - Plugin settings link on plugins page.\n* Add - --use-existing-terms flag for product generator to use existing taxonomy terms.\n* Tweak - Sale dates now generate only future dates when is_on_sale is true.\n* Tweak - Tax status generation is now more dynamic across different product types.\n* Tweak - Order attribution source now uses woocommerce.com instead of woo.com.\n* Tweak - Notify users when no orders are generated due to lack of published products.\n* Tweak - Optimize order date generation performance.\n* Tweak - Update jdenticon to version 2.0.0.\n* Tweak - Update Composer dependencies for PHP 8.5 support.\n* Tweak - Update phpcs-changed to version 2.11.8 for PHP 8.4 compatibility.\n* Dev - Add woocommerce as required plugin in plugin header.\n* Dev - Deprecate generate_term_ids method in favor of more selective cache clearing.\n* Fix - Refund date constraints to prevent future dates.\n* Fix - GitHub Actions MySQL setup to use service containers for better reliability.\n* Fix - Allow attribution on orders with no product items.\n\n2025-03-25 - version 1.2.2\n* Add - Add date range arguments to admin UI for generating data.\n* Avoid fatal errors that sporadically occurred while generating taxonomy terms.\n* De-couple taxonomy term generation from product generation.\n* Ensure transactional emails are disabled before generating test content.\n\n2024-12-05 - version 1.2.1\n* Add - Support for campaign order attribution data.\n* Fix - Remove unknown from get_random_device_type() output.\n* Fix fatal when generating a large amount of orders, which increases the chances of hitting the empty locale issue.\n* Fixes progress bar feedback when generating customers via WP-CLI.\n* Set paid and completed dates based on order status.\n* Tweak - Upgrade fakerphp to latest version to address PHP 8.4 compatibility.\n\n= 1.2.0 - 2024-07-12 =\n* Add - --country and --type arguments for the `generate customers` command.\n* Add - customer generator attempts to localize data based on the specified country.\n* Add - orders will now include order attribution meta data.\n* Add - a progress bar in the Web UI.\n* Add - all generators now use a `batch` function under the hood when generating multiple items.\n* Change - customer generator defaults to only using countries that the store is configured to sell to.\n* Change - customer generator attempts to keep data consistent between name, username, and email address.\n* Change - coupon generator now generates more unique coupon codes.\n* Change - background process for the Web UI now generates items in batches instead of one at a time.\n* Change - menu item under WP Admin > Tools is now just \"Smooth Generator\" for better space efficiency.\n* Dev - update build tools, remove Grunt.\n* Fix - coupon generator will always generate the specified number of coupons.\n\n= 1.1.0 - 2023-03-14 =\n* Add - some generated orders will now include fees.\n* Add - the possibility for billing, shipping, and location addresses to be different in orders.\n* Add - declare compatibility with WooCommerce's High Performance Order Storage feature.\n* Add - all CLI commands now show elapsed time upon completion.\n* Add - introduce --type argument to the `generate products` command.\n* Add - more music video possibilities on the Smooth Generator admin screen.\n* Add - new generator for terms in the product categories and product tags taxonomies.\n* Dev - update PHP version requirement to 7.4.\n* Fix - ensure emails are disabled during generation.\n* Fix - add missing documentation about the coupons CLI command to the readme.\n\n= 1.0.5 - 2022-06-30 =\n* Fix - Lower version requirement from PHP 8.0.2 to PHP 7.1.\n\n= 1.0.4 - 2021-12-15 =\n* Add - coupon generator and a new option for orders to allow for coupon generation.\n* Add - use product name to generate more realistic product term names.\n* Fix - include jdenticon package in generated zip.\n\n= 1.0.3 - 2021-08-12 =\n* Add - --status argument to `generate orders` command\n* Add - UI support for generating products and orders\n* Dev - update Composer support for V2\n* Fix - reduce product generation time by reducing the maximum number of attribute terms on variable products\n* Fix - disable all email notifications on customer and order generation\n\n\n= 1.0.2 - 2020-11-19 =\n* Change log starts.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"woocommerce/wc-smooth-generator\",\n  \"description\": \"A smooth product, order, customer, and coupon generator for WooCommerce.\",\n  \"homepage\": \"https://woocommerce.com/\",\n  \"type\": \"wordpress-plugin\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"prefer-stable\": true,\n  \"minimum-stability\": \"dev\",\n  \"require\": {\n    \"php\": \">=7.4\",\n    \"psr/container\": \"1.0.0\",\n    \"composer/installers\": \"~1.2\",\n    \"fakerphp/faker\": \"^1.24.0\",\n    \"jdenticon/jdenticon\": \"^2.0.0\",\n    \"mbezhanov/faker-provider-collection\": \"^2.0.1\",\n    \"symfony/deprecation-contracts\": \"^2.2\"\n  },\n  \"require-dev\": {\n    \"woocommerce/woocommerce-sniffs\": \"*\",\n    \"sirbrillig/phpcs-changed\": \"^2.11.8\",\n    \"phpunit/phpunit\": \"^9.5 || ^10.0 || ^11.0\",\n    \"yoast/phpunit-polyfills\": \"^1.0 || ^2.0\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\"WC\\\\SmoothGenerator\\\\\": \"includes/\"}\n  },\n  \"autoload-dev\": {\n    \"psr-4\": {\"WC\\\\SmoothGenerator\\\\Tests\\\\\": \"tests/Unit\"}\n  },\n  \"scripts\": {\n    \"test-unit\": \"./vendor/bin/phpunit\",\n    \"phpcs\": [\n      \"vendor/bin/phpcs\"\n    ],\n    \"phpcbf\": [\n      \"vendor/bin/phpcbf\"\n    ],\n    \"lint\": [\n      \"chg=$(git diff --relative --name-only -- '*.php'); [[ -z $chg ]] || phpcs-changed -s --git --git-unstaged $chg\"\n    ],\n    \"lint-staged\": [\n      \"chg=$(git diff HEAD --relative --name-only -- '*.php'); [[ -z $chg ]] || phpcs-changed -s --git $chg\"\n    ],\n    \"lint-branch\": [\n      \"sh ./bin/lint-branch.sh\"\n    ]\n  },\n  \"extra\": {\n    \"scripts-description\": {\n      \"phpcs\": \"Analyze code against the WordPress coding standards with PHP_CodeSniffer\",\n      \"phpcbf\": \"Fix coding standards warnings/errors automatically with PHP Code Beautifier\"\n    }\n  },\n  \"archive\": {\n    \"exclude\": [\n      \"/.github\",\n      \"/.husky\",\n      \"/bin\",\n      \"/node_modules\",\n      \"/tests\",\n      \"composer.*\",\n      \"package*.json\",\n      \"phpcs*\",\n      \"phpunit*\",\n      \"TESTING.md\",\n      \".*\",\n      \"!vendor/autoload.php\",\n      \"!vendor/composer\",\n      \"!vendor/fakerphp\",\n      \"!vendor/jdenticon\",\n      \"!vendor/mbezhanov\",\n      \"!vendor/symfony\"\n    ]\n  },\n  \"config\": {\n    \"allow-plugins\": {\n      \"composer/installers\": true,\n      \"dealerdirect/phpcodesniffer-composer-installer\": true\n    }\n  }\n}\n"
  },
  {
    "path": "includes/Admin/AsyncJob.php",
    "content": "<?php\n\nnamespace WC\\SmoothGenerator\\Admin;\n\n/**\n * Class AsyncJob.\n *\n * A Record Object to hold the current state of an async job.\n */\nclass AsyncJob {\n\t/**\n\t * The slug of the generator.\n\t *\n\t * @var string\n\t */\n\tpublic string $generator_slug = '';\n\n\t/**\n\t * The total number of objects to generate.\n\t *\n\t * @var int\n\t */\n\tpublic int $amount = 0;\n\n\t/**\n\t * Additional args for generating the objects.\n\t *\n\t * @var array\n\t */\n\tpublic array $args = array();\n\n\t/**\n\t * The number of objects already generated.\n\t *\n\t * @var int\n\t */\n\tpublic int $processed = 0;\n\n\t/**\n\t * The number of objects that still need to be generated.\n\t *\n\t * @var int\n\t */\n\tpublic int $pending = 0;\n\n\t/**\n\t * AsyncJob class.\n\t *\n\t * @param array $data\n\t */\n\tpublic function __construct( array $data = array() ) {\n\t\t$defaults = array(\n\t\t\t'generator_slug' => $this->generator_slug,\n\t\t\t'amount'         => $this->amount,\n\t\t\t'args'           => $this->args,\n\t\t\t'processed'      => $this->processed,\n\t\t\t'pending'        => $this->pending,\n\t\t);\n\t\t$data     = wp_parse_args( $data, $defaults );\n\n\t\tlist(\n\t\t\t'generator_slug' => $this->generator_slug,\n\t\t\t'amount'         => $this->amount,\n\t\t\t'args'           => $this->args,\n\t\t\t'processed'      => $this->processed,\n\t\t\t'pending'        => $this->pending\n\t\t) = $data;\n\t}\n}\n"
  },
  {
    "path": "includes/Admin/BatchProcessor.php",
    "content": "<?php\n\nnamespace WC\\SmoothGenerator\\Admin;\n\nuse Automattic\\WooCommerce\\Internal\\BatchProcessing\\{ BatchProcessorInterface, BatchProcessingController };\nuse WC\\SmoothGenerator\\Router;\n\n/**\n * Class BatchProcessor.\n *\n * A class for asynchronously generating batches of objects using WooCommerce's internal batch processing tool.\n * (This might break if changes are made to the tool.)\n */\nclass BatchProcessor implements BatchProcessorInterface {\n\t/**\n\t * The key used to store the state of the current job in the options table.\n\t */\n\tconst OPTION_KEY = 'smoothgenerator_async_job';\n\n\t/**\n\t * Get the state of the current job.\n\t *\n\t * @return ?AsyncJob Null if there is no current job.\n\t */\n\tpublic static function get_current_job() {\n\t\t$current_job = get_option( self::OPTION_KEY, null );\n\n\t\tif ( ! $current_job instanceof AsyncJob && wc_get_container()->get( BatchProcessingController::class )->is_enqueued( self::class ) ) {\n\t\t\twc_get_container()->get( BatchProcessingController::class )->remove_processor( self::class );\n\t\t} elseif ( $current_job instanceof AsyncJob && ! wc_get_container()->get( BatchProcessingController::class )->is_enqueued( self::class ) ) {\n\t\t\tself::delete_current_job();\n\t\t\t$current_job = null;\n\t\t}\n\n\t\treturn $current_job;\n\t}\n\n\t/**\n\t * Create a new AsyncJob object.\n\t *\n\t * @param string $generator_slug The slug identifier of the generator to use.\n\t * @param int    $amount         The number of objects to generate.\n\t * @param array  $args           Additional args for object generation.\n\t *\n\t * @return AsyncJob|\\WP_Error\n\t */\n\tpublic static function create_new_job( string $generator_slug, int $amount, array $args = array() ) {\n\t\tif ( self::get_current_job() instanceof AsyncJob ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_async_job_already_exists',\n\t\t\t\t'Can\\'t create a new Smooth Generator job because one is already in progress.'\n\t\t\t);\n\t\t}\n\n\t\t$job = new AsyncJob( array(\n\t\t\t'generator_slug' => $generator_slug,\n\t\t\t'amount'         => $amount,\n\t\t\t'args'           => $args,\n\t\t\t'pending'        => $amount,\n\t\t) );\n\n\t\tupdate_option( self::OPTION_KEY, $job, false );\n\n\t\twc_get_container()->get( BatchProcessingController::class )->enqueue_processor( self::class );\n\n\t\treturn $job;\n\t}\n\n\t/**\n\t * Update the state of the current job.\n\t *\n\t * @param int $processed The amount to change the state values by.\n\t *\n\t * @return AsyncJob|\\WP_Error\n\t */\n\tpublic static function update_current_job( int $processed ) {\n\t\t$current_job = self::get_current_job();\n\n\t\tif ( ! $current_job instanceof AsyncJob ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_async_job_does_not_exist',\n\t\t\t\t'There is no Smooth Generator job to update.'\n\t\t\t);\n\t\t}\n\n\t\t$current_job->processed += $processed;\n\t\t$current_job->pending    = max( $current_job->pending - $processed, 0 );\n\n\t\tupdate_option( self::OPTION_KEY, $current_job, false );\n\n\t\treturn $current_job;\n\t}\n\n\t/**\n\t * Delete the AsyncJob object.\n\t *\n\t * @return bool\n\t */\n\tpublic static function delete_current_job() {\n\t\twc_get_container()->get( BatchProcessingController::class )->remove_processor( self::class );\n\t\tdelete_option( self::OPTION_KEY );\n\t}\n\n\t/**\n\t * Get a user-friendly name for this processor.\n\t *\n\t * @return string Name of the processor.\n\t */\n\tpublic function get_name(): string {\n\t\treturn 'Smooth Generator';\n\t}\n\n\t/**\n\t * Get a user-friendly description for this processor.\n\t *\n\t * @return string Description of what this processor does.\n\t */\n\tpublic function get_description(): string {\n\t\treturn 'Generates various types of WooCommerce data objects with randomized data for use in testing.';\n\t}\n\n\t/**\n\t * Get the total number of pending items that require processing.\n\t * Once an item is successfully processed by 'process_batch' it shouldn't be included in this count.\n\t *\n\t * Note that the once the processor is enqueued the batch processor controller will keep\n\t * invoking `get_next_batch_to_process` and `process_batch` repeatedly until this method returns zero.\n\t *\n\t * @return int Number of items pending processing.\n\t */\n\tpublic function get_total_pending_count(): int {\n\t\t$current_job = self::get_current_job();\n\n\t\tif ( ! $current_job instanceof AsyncJob ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn $current_job->pending;\n\t}\n\n\t/**\n\t * Returns the next batch of items that need to be processed.\n\t *\n\t * A batch item can be anything needed to identify the actual processing to be done,\n\t * but whenever possible items should be numbers (e.g. database record ids)\n\t * or at least strings, to ease troubleshooting and logging in case of problems.\n\t *\n\t * The size of the batch returned can be less than $size if there aren't that\n\t * many items pending processing (and it can be zero if there isn't anything to process),\n\t * but the size should always be consistent with what 'get_total_pending_count' returns\n\t * (i.e. the size of the returned batch shouldn't be larger than the pending items count).\n\t *\n\t * @param int $size Maximum size of the batch to be returned.\n\t *\n\t * @return array Batch of items to process, containing $size or less items.\n\t */\n\tpublic function get_next_batch_to_process( int $size ): array {\n\t\t$current_job = self::get_current_job();\n\t\t$max_batch   = self::get_default_batch_size();\n\n\t\tif ( ! $current_job instanceof AsyncJob ) {\n\t\t\t$current_job = new AsyncJob();\n\t\t}\n\n\t\t$amount = min( $size, $current_job->pending, $max_batch );\n\n\t\t// The batch processing controller counts items in the array to determine if there are still pending items.\n\t\tif ( $amount < 1 ) {\n\t\t\treturn array();\n\t\t}\n\n\t\treturn array(\n\t\t\t'generator_slug' => $current_job->generator_slug,\n\t\t\t'amount'         => $amount,\n\t\t\t'args'           => $current_job->args,\n\t\t);\n\t}\n\n\t/**\n\t * Process data for the supplied batch.\n\t *\n\t * This method should be prepared to receive items that don't actually need processing\n\t * (because they have been processed before) and ignore them, but if at least\n\t * one of the batch items that actually need processing can't be processed, an exception should be thrown.\n\t *\n\t * Once an item has been processed it shouldn't be counted in 'get_total_pending_count'\n\t * nor included in 'get_next_batch_to_process' anymore (unless something happens that causes it\n\t * to actually require further processing).\n\t *\n\t * @throw \\Exception Something went wrong while processing the batch.\n\t *\n\t * @param array $batch Batch to process, as returned by 'get_next_batch_to_process'.\n\t */\n\tpublic function process_batch( array $batch ): void {\n\t\tlist( 'generator_slug' => $slug, 'amount' => $amount, 'args' => $args ) = $batch;\n\n\t\t$result = Router::generate_batch( $slug, $amount, $args );\n\n\t\tif ( is_wp_error( $result ) ) {\n\t\t\tthrow new \\Exception( $result->get_error_message() );\n\t\t}\n\n\t\tself::update_current_job( count( $result ) );\n\t}\n\n\t/**\n\t * Default (preferred) batch size to pass to 'get_next_batch_to_process'.\n\t * The controller will pass this size unless it's externally configured\n\t * to use a different size.\n\t *\n\t * @return int Default batch size.\n\t */\n\tpublic function get_default_batch_size(): int {\n\t\t$current_job = self::get_current_job() ?: new AsyncJob();\n\t\t$generator   = Router::get_generator_class( $current_job->generator_slug );\n\n\t\tif ( is_wp_error( $generator ) ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn $generator::MAX_BATCH_SIZE;\n\t}\n}\n"
  },
  {
    "path": "includes/Admin/Settings.php",
    "content": "<?php\n/**\n * Plugin admin settings\n *\n * @package SmoothGenerator\\Admin\\Classes\n */\n\nnamespace WC\\SmoothGenerator\\Admin;\n\n/**\n *  Initializes and manages the settings screen.\n */\nclass Settings {\n\n\tconst DEFAULT_NUM_PRODUCTS           = 10;\n\tconst DEFAULT_NUM_ORDERS             = 10;\n\n\t/**\n\t *  Set up hooks.\n\t */\n\tpublic static function init() {\n\t\tadd_action( 'admin_menu', array( __CLASS__, 'register_admin_menu' ) );\n\t\tadd_filter( 'heartbeat_received', array( __CLASS__, 'receive_heartbeat' ), 10, 3 );\n\t}\n\n\t/**\n\t * Register the admin menu and screen.\n\t */\n\tpublic static function register_admin_menu() {\n\t\t$hook = add_management_page(\n\t\t\t'WooCommerce Smooth Generator',\n\t\t\t'Smooth Generator',\n\t\t\t'install_plugins',\n\t\t\t'smoothgenerator',\n\t\t\tarray( __CLASS__, 'render_admin_page' )\n\t\t);\n\n\t\tadd_action( \"load-$hook\", array( __CLASS__, 'process_page_submit' ) );\n\t}\n\n\t/**\n\t * Render the admin page.\n\t */\n\tpublic static function render_admin_page() {\n\t\t$current_job = self::get_current_job();\n\n\t\t$generate_button_atts = $current_job instanceof AsyncJob ? array( 'disabled' => true ) : array();\n\t\t$cancel_button_atts   = ! $current_job instanceof AsyncJob ? array( 'disabled' => true ) : array();\n\n\t\t?>\n\t\t<h1>WooCommerce Smooth Generator</h1>\n\t\t<p class=\"description\">\n\t\t\tGenerate randomized WooCommerce data for testing.\n\t\t</p>\n\n\t\t<?php echo self::while_you_wait(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>\n\n\t\t<?php if ( $current_job instanceof AsyncJob ) : ?>\n\t\t\t<div id=\"smoothgenerator-progress\">\n\t\t\t\t<label for=\"smoothgenerator-progress-bar\" style=\"display: block;\">\n\t\t\t\t\t<?php\n\t\t\t\t\tprintf(\n\t\t\t\t\t\t'Generating %s %s&hellip;',\n\t\t\t\t\t\tnumber_format_i18n( $current_job->amount ),\n\t\t\t\t\t\tesc_html( $current_job->generator_slug )\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\t\t\t\t</label>\n\t\t\t\t<progress\n\t\t\t\t\tid=\"smoothgenerator-progress-bar\"\n\t\t\t\t\tmax=\"<?php echo esc_attr( $current_job->amount ); ?>\"\n\t\t\t\t\tvalue=\"<?php echo $current_job->processed ? esc_attr( $current_job->processed ) : ''; ?>\"\n\t\t\t\t\tstyle=\"width: 560px;\"\n\t\t\t\t>\n\t\t\t\t\t<?php\n\t\t\t\t\tprintf(\n\t\t\t\t\t\t'%d out of %d',\n\t\t\t\t\t\tesc_html( $current_job->processed ),\n\t\t\t\t\t\tesc_html( $current_job->amount ),\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\t\t\t\t</progress>\n\t\t\t</div>\n\t\t<?php elseif ( filter_input( INPUT_POST, 'cancel_job' ) ) : ?>\n\t\t\t<div class=\"notice notice-error inline-notice is-dismissible\" style=\"margin-left: 0;\">\n\t\t\t\t<p>Current job canceled.</p>\n\t\t\t</div>\n\t\t<?php endif; ?>\n\n\t\t<form method=\"post\">\n\t\t\t<?php wp_nonce_field( 'generate', 'smoothgenerator_nonce' ); ?>\n\t\t\t<h2>Generate products</h2>\n\t\t\t<p>\n\t\t\t\t<label for=\"generate_products_input\" class=\"screen-reader-text\">Number of products to generate</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"generate_products_input\"\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tname=\"num_products_to_generate\"\n\t\t\t\t\tvalue=\"<?php echo esc_attr( self::DEFAULT_NUM_PRODUCTS ); ?>\"\n\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t<?php disabled( $current_job instanceof AsyncJob ); ?>\n\t\t\t\t/>\n\t\t\t\t<?php\n\t\t\t\tsubmit_button(\n\t\t\t\t\t'Generate',\n\t\t\t\t\t'primary',\n\t\t\t\t\t'generate_products',\n\t\t\t\t\tfalse,\n\t\t\t\t\t$generate_button_atts\n\t\t\t\t);\n\t\t\t\t?>\n\t\t\t</p>\n\n\t\t\t<h2>Generate orders</h2>\n\t\t\t<p>\n\t\t\t\t<label for=\"generate_orders_input\" class=\"screen-reader-text\">Number of orders to generate</label>\n\t\t\t\t<input\n\t\t\t\t\tid=\"generate_orders_input\"\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tname=\"num_orders_to_generate\"\n\t\t\t\t\tvalue=\"<?php echo esc_attr( self::DEFAULT_NUM_ORDERS ); ?>\"\n\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t<?php disabled( $current_job instanceof AsyncJob ); ?>\n\t\t\t\t/>\n\t\t\t\t<?php\n\t\t\t\tsubmit_button(\n\t\t\t\t\t'Generate',\n\t\t\t\t\t'primary',\n\t\t\t\t\t'generate_orders',\n\t\t\t\t\tfalse,\n\t\t\t\t\t$generate_button_atts\n\t\t\t\t);\n\t\t\t\t?>\n\t\t\t</p>\n\n\t\t\t<h2>Advanced Options</h2>\n\t\t\t<p>\n\t\t\t\t<label>\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\tid=\"use_date_range\"\n\t\t\t\t\t\tname=\"use_date_range\"\n\t\t\t\t\t\t<?php disabled( $current_job instanceof AsyncJob ); ?>\n\t\t\t\t\t/>\n\t\t\t\t\tSpecify date range for generation\n\t\t\t\t</label>\n\t\t\t</p>\n\t\t\t<div id=\"date_range_inputs\" style=\"display: none;\">\n\t\t\t\t<p>\n\t\t\t\t\t<label for=\"generate_start_date_input\">Start date</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"generate_start_date_input\"\n\t\t\t\t\t\ttype=\"date\"\n\t\t\t\t\t\tname=\"start_date\"\n\t\t\t\t\t\tvalue=\"<?php echo esc_attr( date( 'Y-m-d' ) ); ?>\"\n\t\t\t\t\t\t<?php disabled( $current_job instanceof AsyncJob ); ?>\n\t\t\t\t\t/>\n\t\t\t\t\t<label for=\"generate_end_date_input\">End date</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"generate_end_date_input\"\n\t\t\t\t\t\ttype=\"date\"\n\t\t\t\t\t\tname=\"end_date\"\n\t\t\t\t\t\tvalue=\"<?php echo esc_attr( date( 'Y-m-d' ) ); ?>\"\n\t\t\t\t\t\t<?php disabled( $current_job instanceof AsyncJob ); ?>\n\t\t\t\t\t/>\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<?php\n\t\t\tsubmit_button(\n\t\t\t\t'Cancel current job',\n\t\t\t\t'secondary',\n\t\t\t\t'cancel_job',\n\t\t\t\ttrue,\n\t\t\t\t$cancel_button_atts\n\t\t\t);\n\t\t\t?>\n\t\t</form>\n\t\t<?php\n\n\t\tself::heartbeat_script();\n\t\tself::date_range_toggle_script();\n\t}\n\n\t/**\n\t * Script to toggle date range inputs visibility.\n\t *\n\t * @return void\n\t */\n\tprotected static function date_range_toggle_script() {\n\t\t?>\n\t\t<script>\n\t\t\t( function( $ ) {\n\t\t\t\t$( '#use_date_range' ).on( 'change', function() {\n\t\t\t\t\t$( '#date_range_inputs' ).toggle( this.checked );\n\t\t\t\t} );\n\t\t\t} )( jQuery );\n\t\t</script>\n\t\t<?php\n\t}\n\n\t/**\n\t * Script to interact with heartbeat and run the progress bar.\n\t *\n\t * @return void\n\t */\n\tprotected static function heartbeat_script() {\n\t\t?>\n\t\t<script>\n\t\t\t( function( $ ) {\n\t\t\t\tconst $document = $( document );\n\t\t\t\tconst $progress = $( '#smoothgenerator-progress-bar' );\n\t\t\t\tconst $controls = $( '[id^=\"generate_\"], #use_date_range, #date_range_inputs input' );\n\t\t\t\tconst $cancel   = $( '#cancel_job' );\n\n\t\t\t\t$document.on( 'ready', function () {\n\t\t\t\t\twp.heartbeat.disableSuspend();\n\t\t\t\t\twp.heartbeat.interval( 'fast' );\n\t\t\t\t\twp.heartbeat.connectNow();\n\t\t\t\t} );\n\n\t\t\t\t$document.on( 'heartbeat-send', function ( event, data ) {\n\t\t\t\t\tdata.smoothgenerator = 'check_async_job_progress';\n\t\t\t\t} );\n\n\t\t\t\t$document.on( 'heartbeat-tick', function ( event, data ) {\n\t\t\t\t\t// Heartbeat and other admin-ajax calls don't trigger wp-cron, so we have to do it manually.\n\t\t\t\t\t$.ajax( {\n\t\t\t\t\t\turl: data.smoothgenerator_ping_cron,\n\t\t\t\t\t\tmethod: 'get',\n\t\t\t\t\t\ttimeout: 5000,\n\t\t\t\t\t\tdataType: 'html'\n\t\t\t\t\t} );\n\n\t\t\t\t\tif ( 'object' === typeof data.smoothgenerator_async_job_progress ) {\n\t\t\t\t\t\tconst value = parseInt( data.smoothgenerator_async_job_progress.processed );\n\t\t\t\t\t\tif ( value > 0 ) {\n\t\t\t\t\t\t\t$progress.prop( 'value', value );\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if ( 'complete' === data.smoothgenerator_async_job_progress && $progress.is( ':visible' ) ) {\n\t\t\t\t\t\t$progress.prop( 'value', $progress.prop( 'max' ) );\n\t\t\t\t\t\t$progress.parent().append( '✅' );\n\t\t\t\t\t\t$progress.siblings( 'label' ).first().append( ' Done!' );\n\t\t\t\t\t\t$controls.add( $cancel ).prop( 'disabled', function ( i, val ) {\n\t\t\t\t\t\t\treturn ! val;\n\t\t\t\t\t\t} );\n\t\t\t\t\t\t$document.off( 'heartbeat-send' );\n\t\t\t\t\t\t$document.off( 'heartbeat-tick' );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t} )( jQuery );\n\t\t</script>\n\t<?php\n\t}\n\n\t/**\n\t * Callback to send data for updating the progress bar.\n\t *\n\t * @param array  $response  The data that will be sent back to heartbeat.\n\t * @param array  $data      The incoming data from heartbeat.\n\t * @param string $screen_id The ID of the current WP Admin screen.\n\t *\n\t * @return array\n\t */\n\tpublic static function receive_heartbeat( array $response, array $data, $screen_id ) {\n\t\tif ( 'tools_page_smoothgenerator' !== $screen_id || empty( $data['smoothgenerator'] ) ) {\n\t\t\treturn $response;\n\t\t}\n\n\t\t$current_job = self::get_current_job();\n\n\t\tif ( $current_job instanceof AsyncJob ) {\n\t\t\t$response['smoothgenerator_async_job_progress'] = $current_job;\n\t\t\t$response['smoothgenerator_ping_cron']          = site_url( 'wp-cron.php' );\n\t\t} else {\n\t\t\t$response['smoothgenerator_async_job_progress'] = 'complete';\n\t\t}\n\n\t\treturn $response;\n\t}\n\n\t/**\n\t * Process the generation.\n\t */\n\tpublic static function process_page_submit() {\n\t\t$args = array();\n\t\t\n\t\tif ( ! empty( $_POST['use_date_range'] ) ) {\n\t\t\t$args['date-start'] = sanitize_text_field( $_POST['start_date'] );\n\t\t\t$args['date-end'] = sanitize_text_field( $_POST['end_date'] );\n\t\t}\n\n\t\tif ( ! empty( $_POST['generate_products'] ) && ! empty( $_POST['num_products_to_generate'] ) ) {\n\t\t\tcheck_admin_referer( 'generate', 'smoothgenerator_nonce' );\n\t\t\t$num_to_generate = absint( $_POST['num_products_to_generate'] );\n\t\t\tBatchProcessor::create_new_job( 'products', $num_to_generate, $args );\n\t\t} else if ( ! empty( $_POST['generate_orders'] ) && ! empty( $_POST['num_orders_to_generate'] ) ) {\n\t\t\tcheck_admin_referer( 'generate', 'smoothgenerator_nonce' );\n\t\t\t$num_to_generate = absint( $_POST['num_orders_to_generate'] );\n\t\t\tBatchProcessor::create_new_job( 'orders', $num_to_generate, $args );\n\t\t} else if ( ! empty( $_POST['cancel_job'] ) ) {\n\t\t\tcheck_admin_referer( 'generate', 'smoothgenerator_nonce' );\n\t\t\tBatchProcessor::delete_current_job();\n\t\t}\n\t}\n\n\t/**\n\t * Get the state of the current background job.\n\t *\n\t * @return AsyncJob|null\n\t */\n\tprotected static function get_current_job() {\n\t\treturn BatchProcessor::get_current_job();\n\t}\n\n\t/**\n\t * Render some entertainment while waiting for the generator to finish.\n\t *\n\t * @return string\n\t */\n\tprotected static function while_you_wait() {\n\t\t$current_job = self::get_current_job();\n\t\t$content     = '';\n\n\t\tif ( filter_input( INPUT_POST, 'smoothgenerator_nonce' ) || $current_job instanceof AsyncJob ) {\n\t\t\tif ( filter_input( INPUT_POST, 'cancel_job' ) ) {\n\t\t\t\t$embed = 'NF9Y3GVuPfY';\n\t\t\t} else {\n\t\t\t\t$videos    = array(\n\t\t\t\t\t'4TYv2PhG89A',\n\t\t\t\t\t'6Whgn_iE5uc',\n\t\t\t\t\t'h_D3VFfhvs4',\n\t\t\t\t\t'QcjAXI4jANw',\n\t\t\t\t);\n\t\t\t\t$next_wait = filter_input( INPUT_COOKIE, 'smoothgenerator_next_wait' );\n\t\t\t\tif ( ! isset( $videos[ $next_wait ] ) ) {\n\t\t\t\t\t$next_wait = 0;\n\t\t\t\t}\n\t\t\t\t$embed = $videos[ $next_wait ];\n\t\t\t\t$next_wait ++;\n\t\t\t\tsetcookie(\n\t\t\t\t\t'smoothgenerator_next_wait',\n\t\t\t\t\t$next_wait,\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'expires'  => time() + WEEK_IN_SECONDS,\n\t\t\t\t\t\t'path'     => ADMIN_COOKIE_PATH,\n\t\t\t\t\t\t'domain'   => COOKIE_DOMAIN,\n\t\t\t\t\t\t'secure'   => is_ssl(),\n\t\t\t\t\t\t'samesite' => 'strict',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t$content = <<<\"EMBED\"\n<h2>While you wait...</h2>\n<div class=\"wp-block-embed__wrapper\" style=\"margin: 2em 0;\"><iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/$embed?autoplay=1&fs=0&iv_load_policy=3&showinfo=0&rel=0&cc_load_policy=0&start=0&end=0\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen>></iframe></div>\nEMBED;\n\t\t}\n\n\t\treturn $content;\n\t}\n}\n"
  },
  {
    "path": "includes/CLI.php",
    "content": "<?php\n/**\n * WP-CLI functionality.\n *\n * @package SmoothGenerator\\Classes\n */\n\nnamespace WC\\SmoothGenerator;\n\nuse WP_CLI, WP_CLI_Command;\n\n/**\n * WP-CLI Integration class\n */\nclass CLI extends WP_CLI_Command {\n\t/**\n\t * Generate products.\n\t *\n\t * @param array $args Arguments specified.\n\t * @param array $assoc_args Associative arguments specified.\n\t */\n\tpublic static function products( $args, $assoc_args ) {\n\t\tlist( $amount ) = $args;\n\t\t$amount = absint( $amount );\n\n\t\t$time_start = microtime( true );\n\n\t\tWP_CLI::line( 'Initializing...' );\n\n\t\t// Pre-generate images. Min 20, max 100.\n\t\tGenerator\\Product::seed_images( min( $amount + 19, 100 ) );\n\n\t\t$progress = \\WP_CLI\\Utils\\make_progress_bar( 'Generating products', $amount );\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_product_generated',\n\t\t\tfunction () use ( $progress ) {\n\t\t\t\t$progress->tick();\n\t\t\t}\n\t\t);\n\n\t\t$remaining_amount = $amount;\n\t\t$generated        = 0;\n\n\t\twhile ( $remaining_amount > 0 ) {\n\t\t\t$batch = min( $remaining_amount, Generator\\Product::MAX_BATCH_SIZE );\n\n\t\t\t$result = Generator\\Product::batch( $batch, $assoc_args );\n\n\t\t\tif ( is_wp_error( $result ) ) {\n\t\t\t\tWP_CLI::error( $result );\n\t\t\t}\n\n\t\t\t$generated        += count( $result );\n\t\t\t$remaining_amount -= $batch;\n\t\t}\n\n\t\t$progress->finish();\n\n\t\t$time_end       = microtime( true );\n\t\t$execution_time = round( ( $time_end - $time_start ), 2 );\n\t\t$display_time   = $execution_time < 60 ? $execution_time . ' seconds' : human_time_diff( $time_start, $time_end );\n\n\t\tWP_CLI::success( $generated . ' products generated in ' . $display_time );\n\t}\n\n\t/**\n\t * Generate orders.\n\t *\n\t * @param array $args Arguments specified.\n\t * @param array $assoc_args Associative arguments specified.\n\t */\n\tpublic static function orders( $args, $assoc_args ) {\n\t\tlist( $amount ) = $args;\n\t\t$amount = absint( $amount );\n\n\t\t$time_start = microtime( true );\n\n\t\tif ( ! empty( $assoc_args['status'] ) ) {\n\t\t\t$status = $assoc_args['status'];\n\t\t\tif ( ! wc_is_order_status( 'wc-' . $status ) ) {\n\t\t\t\tWP_CLI::error( \"The argument \\\"$status\\\" is not a valid order status.\" );\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t$progress = \\WP_CLI\\Utils\\make_progress_bar( 'Generating orders', $amount );\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_order_generated',\n\t\t\tfunction () use ( $progress ) {\n\t\t\t\t$progress->tick();\n\t\t\t}\n\t\t);\n\n\t\t$remaining_amount = $amount;\n\t\t$generated        = 0;\n\n\t\twhile ( $remaining_amount > 0 ) {\n\t\t\t$batch = min( $remaining_amount, Generator\\Order::MAX_BATCH_SIZE );\n\n\t\t\t$result = Generator\\Order::batch( $batch, $assoc_args );\n\n\t\t\tif ( is_wp_error( $result ) ) {\n\t\t\t\tWP_CLI::error( $result );\n\t\t\t}\n\n\t\t\t$generated        += count( $result );\n\t\t\t$remaining_amount -= $batch;\n\t\t}\n\n\t\t$progress->finish();\n\n\t\t$time_end       = microtime( true );\n\t\t$execution_time = round( ( $time_end - $time_start ), 2 );\n\t\t$display_time   = $execution_time < 60 ? $execution_time . ' seconds' : human_time_diff( $time_start, $time_end );\n\n\t\tif ( $generated === 0 && $amount > 0 ) {\n\t\t\tWP_CLI::error( 'No orders were generated. Make sure there are published products in your store.' );\n\t\t}\n\n\t\tWP_CLI::success( $generated . ' orders generated in ' . $display_time );\n\t}\n\n\t/**\n\t * Generate customers.\n\t *\n\t * @param array $args Arguments specified.\n\t * @param array $assoc_args Associative arguments specified.\n\t */\n\tpublic static function customers( $args, $assoc_args ) {\n\t\tlist( $amount ) = $args;\n\t\t$amount = absint( $amount );\n\n\t\t$time_start = microtime( true );\n\n\t\t$progress = \\WP_CLI\\Utils\\make_progress_bar( 'Generating customers', $amount );\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_customer_generated',\n\t\t\tfunction () use ( $progress ) {\n\t\t\t\t$progress->tick();\n\t\t\t}\n\t\t);\n\n\t\t$remaining_amount = $amount;\n\t\t$generated        = 0;\n\n\t\twhile ( $remaining_amount > 0 ) {\n\t\t\t$batch = min( $remaining_amount, Generator\\Customer::MAX_BATCH_SIZE );\n\n\t\t\t$result = Generator\\Customer::batch( $batch, $assoc_args );\n\n\t\t\tif ( is_wp_error( $result ) ) {\n\t\t\t\tWP_CLI::error( $result );\n\t\t\t}\n\n\t\t\t$generated        += count( $result );\n\t\t\t$remaining_amount -= $batch;\n\t\t}\n\n\t\t$progress->finish();\n\n\t\t$time_end       = microtime( true );\n\t\t$execution_time = round( ( $time_end - $time_start ), 2 );\n\t\t$display_time   = $execution_time < 60 ? $execution_time . ' seconds' : human_time_diff( $time_start, $time_end );\n\n\t\tWP_CLI::success( $generated . ' customers generated in ' . $display_time );\n\t}\n\n\t/**\n\t * Generate coupons.\n\t *\n\t * @param array $args Arguments specified.\n\t * @param array $assoc_args Associative arguments specified.\n\t */\n\tpublic static function coupons( $args, $assoc_args ) {\n\t\tlist( $amount ) = $args;\n\t\t$amount = absint( $amount );\n\n\t\t$time_start = microtime( true );\n\n\t\t$progress = \\WP_CLI\\Utils\\make_progress_bar( 'Generating coupons', $amount );\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_coupon_generated',\n\t\t\tfunction () use ( $progress ) {\n\t\t\t\t$progress->tick();\n\t\t\t}\n\t\t);\n\n\t\t$remaining_amount = $amount;\n\t\t$generated        = 0;\n\n\t\twhile ( $remaining_amount > 0 ) {\n\t\t\t$batch = min( $remaining_amount, Generator\\Coupon::MAX_BATCH_SIZE );\n\n\t\t\t$result = Generator\\Coupon::batch( $batch, $assoc_args );\n\n\t\t\tif ( is_wp_error( $result ) ) {\n\t\t\t\tWP_CLI::error( $result );\n\t\t\t}\n\n\t\t\t$generated        += count( $result );\n\t\t\t$remaining_amount -= $batch;\n\t\t}\n\n\t\t$progress->finish();\n\n\t\t$time_end       = microtime( true );\n\t\t$execution_time = round( ( $time_end - $time_start ), 2 );\n\t\t$display_time   = $execution_time < 60 ? $execution_time . ' seconds' : human_time_diff( $time_start, $time_end );\n\n\t\tWP_CLI::success( $generated . ' coupons generated in ' . $display_time );\n\t}\n\n\t/**\n\t * Generate terms for the Product Category taxonomy.\n\t *\n\t * @param array $args Arguments specified.\n\t * @param array $assoc_args Associative arguments specified.\n\t */\n\tpublic static function terms( $args, $assoc_args ) {\n\t\tlist( $taxonomy, $amount ) = $args;\n\t\t$amount = absint( $amount );\n\n\t\t$time_start = microtime( true );\n\n\t\t$progress = \\WP_CLI\\Utils\\make_progress_bar( 'Generating terms', $amount );\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_term_generated',\n\t\t\tfunction () use ( $progress ) {\n\t\t\t\t$progress->tick();\n\t\t\t}\n\t\t);\n\n\t\t$remaining_amount = $amount;\n\t\t$generated        = 0;\n\n\t\twhile ( $remaining_amount > 0 ) {\n\t\t\t$batch = min( $remaining_amount, Generator\\Term::MAX_BATCH_SIZE );\n\n\t\t\t$result = Generator\\Term::batch( $amount, $taxonomy, $assoc_args );\n\n\t\t\tif ( is_wp_error( $result ) ) {\n\t\t\t\tWP_CLI::error( $result );\n\t\t\t}\n\n\t\t\t$generated        += count( $result );\n\t\t\t$remaining_amount -= $batch;\n\t\t}\n\n\t\t$progress->finish();\n\n\t\t$time_end       = microtime( true );\n\t\t$execution_time = round( ( $time_end - $time_start ), 2 );\n\t\t$display_time   = $execution_time < 60 ? $execution_time . ' seconds' : human_time_diff( $time_start, $time_end );\n\n\t\tWP_CLI::success( $generated . ' terms generated in ' . $display_time );\n\t}\n}\n\nWP_CLI::add_command( 'wc generate products', array( 'WC\\SmoothGenerator\\CLI', 'products' ), array(\n\t'shortdesc' => 'Generate products.',\n\t'synopsis'  => array(\n\t\tarray(\n\t\t\t'name'        => 'amount',\n\t\t\t'type'        => 'positional',\n\t\t\t'description' => 'The number of products to generate.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => 10,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'type',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Specify one type of product to generate. Otherwise defaults to a mix.',\n\t\t\t'optional'    => true,\n\t\t\t'options'     => array( 'simple', 'variable' ),\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'use-existing-terms',\n\t\t\t'type'        => 'flag',\n\t\t\t'description' => 'Only apply existing categories and tags to products, rather than generating new ones.',\n\t\t\t'optional'    => true,\n\t\t),\n\t),\n\t'longdesc'  => \"## EXAMPLES\\n\\nwc generate products 10\\n\\nwc generate products 20 --type=variable --use-existing-terms\",\n) );\n\nWP_CLI::add_command( 'wc generate orders', array( 'WC\\SmoothGenerator\\CLI', 'orders' ), array(\n\t'shortdesc' => 'Generate orders.',\n\t'synopsis'  => array(\n\t\tarray(\n\t\t\t'name'        => 'amount',\n\t\t\t'type'        => 'positional',\n\t\t\t'description' => 'The number of orders to generate.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => 10,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'date-start',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Randomize the order date using this as the lower limit. Format as YYYY-MM-DD.',\n\t\t\t'optional'    => true,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'date-end',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Randomize the order date using this as the upper limit. Only works in conjunction with date-start. Format as YYYY-MM-DD.',\n\t\t\t'optional'    => true,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'status',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Specify one status for all the generated orders. Otherwise defaults to a mix.',\n\t\t\t'optional'    => true,\n\t\t\t'options'     => array( 'completed', 'processing', 'on-hold', 'failed' ),\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'coupons',\n\t\t\t'type'        => 'flag',\n\t\t\t'description' => 'Create and apply a coupon to each generated order. Equivalent to --coupon-ratio=1.0.',\n\t\t\t'optional'    => true,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'coupon-ratio',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Decimal ratio (0.0-1.0) of orders that should have coupons applied. If no coupons exist, 6 will be created (3 fixed value, 3 percentage). Note: Decimal values are converted to percentages using integer rounding (e.g., 0.505 becomes 50%).',\n\t\t\t'optional'    => true,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'refund-ratio',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Decimal ratio (0.0-1.0) of completed orders that should be refunded (wholly or partially). Note: Decimal values are converted to percentages using integer rounding (e.g., 0.505 becomes 50%).',\n\t\t\t'optional'    => true,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'skip-order-attribution',\n\t\t\t'type'        => 'flag',\n\t\t\t'description' => 'Skip adding order attribution meta to the generated orders.',\n\t\t\t'optional'    => true,\n\t\t)\n\t),\n\t'longdesc'  => \"## EXAMPLES\\n\\nwc generate orders 10\\n\\nwc generate orders 50 --date-start=2020-01-01 --date-end=2022-12-31 --status=completed --coupons\",\n) );\n\nWP_CLI::add_command( 'wc generate customers', array( 'WC\\SmoothGenerator\\CLI', 'customers' ), array(\n\t'shortdesc' => 'Generate customers.',\n\t'synopsis'  => array(\n\t\tarray(\n\t\t\t'name'        => 'amount',\n\t\t\t'type'        => 'positional',\n\t\t\t'description' => 'The number of customers to generate.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => 10,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'country',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'The ISO 3166-1 alpha-2 country code to use for localizing the customer data. If none is specified, any country in the \"Selling location(s)\" setting may be used.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => '',\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'type',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'The type of customer to generate data for. If none is specified, it will be a 70% person, 30% company mix.',\n\t\t\t'optional'    => true,\n\t\t\t'options'     => array( 'company', 'person' ),\n\t\t),\n\t),\n\t'longdesc'  => \"## EXAMPLES\\n\\nwc generate customers 10\\n\\nwc generate customers --country=ES --type=company\",\n) );\n\nWP_CLI::add_command( 'wc generate coupons', array( 'WC\\SmoothGenerator\\CLI', 'coupons' ), array(\n\t'shortdesc' => 'Generate coupons.',\n\t'synopsis'  => array(\n\t\tarray(\n\t\t\t'name'        => 'amount',\n\t\t\t'type'        => 'positional',\n\t\t\t'description' => 'The number of coupons to generate.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => 10,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'min',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Specify the minimum discount of each coupon, as an integer.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => 5,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'max',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Specify the maximum discount of each coupon, as an integer.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => 100,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'discount_type',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'The type of discount for the coupon. If not specified, defaults to WooCommerce default (fixed_cart).',\n\t\t\t'optional'    => true,\n\t\t\t'options'     => array( 'fixed_cart', 'percent' ),\n\t\t),\n\t),\n\t'longdesc'  => \"## EXAMPLES\\n\\nwc generate coupons 10\\n\\nwc generate coupons 50 --min=1 --max=50\\n\\nwc generate coupons 20 --discount_type=percent --min=5 --max=25\",\n) );\n\nWP_CLI::add_command( 'wc generate terms', array( 'WC\\SmoothGenerator\\CLI', 'terms' ), array(\n\t'shortdesc' => 'Generate product categories.',\n\t'synopsis'  => array(\n\t\tarray(\n\t\t\t'name'        => 'taxonomy',\n\t\t\t'type'        => 'positional',\n\t\t\t'description' => 'The taxonomy to generate the terms for.',\n\t\t\t'options'     => array( 'product_cat', 'product_tag' ),\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'amount',\n\t\t\t'type'        => 'positional',\n\t\t\t'description' => 'The number of terms to generate.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => 10,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'max-depth',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'The maximum number of hierarchy levels for the terms. A value of 1 means all categories will be top-level. Max value 5. Only applies to taxonomies that are hierarchical.',\n\t\t\t'optional'    => true,\n\t\t\t'options'     => array( 1, 2, 3, 4, 5 ),\n\t\t\t'default'     => 1,\n\t\t),\n\t\tarray(\n\t\t\t'name'        => 'parent',\n\t\t\t'type'        => 'assoc',\n\t\t\t'description' => 'Specify an existing term ID as the parent for the new terms. Only applies to taxonomies that are hierarchical.',\n\t\t\t'optional'    => true,\n\t\t\t'default'     => 0,\n\t\t),\n\t),\n\t'longdesc' => \"## EXAMPLES\\n\\nwc generate terms product_tag 10\\n\\nwc generate terms product_cat 50 --max-depth=3\",\n) );\n"
  },
  {
    "path": "includes/Generator/Coupon.php",
    "content": "<?php\n/**\n * Customer data generation.\n *\n * @package SmoothGenerator\\Classes\n */\n\nnamespace WC\\SmoothGenerator\\Generator;\n\nuse WC_Data_Store;\n\n/**\n * Customer data generator.\n */\nclass Coupon extends Generator {\n\t/**\n\t * Create a new coupon.\n\t *\n\t * @param bool  $save       Whether to save the new coupon to the database.\n\t * @param array $assoc_args Arguments passed via the CLI for additional customization.\n\t *\n\t * @return \\WC_Coupon|\\WP_Error Coupon object with data populated.\n\t */\n\tpublic static function generate( $save = true, $assoc_args = array() ) {\n\t\tparent::maybe_initialize_generators();\n\n\t\t$defaults = array(\n\t\t\t'min'           => 5,\n\t\t\t'max'           => 100,\n\t\t\t'discount_type' => 'fixed_cart',\n\t\t);\n\n\t\t$args = wp_parse_args( $assoc_args, $defaults );\n\n\t\tlist( 'min' => $min, 'max' => $max ) = filter_var_array(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'min' => array(\n\t\t\t\t\t'filter'  => FILTER_VALIDATE_INT,\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'min_range' => 1,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'max' => array(\n\t\t\t\t\t'filter'  => FILTER_VALIDATE_INT,\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'min_range' => 1,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\tif ( false === $min ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_coupon_invalid_min_max',\n\t\t\t\t'The minimum coupon amount must be a valid positive integer.'\n\t\t\t);\n\t\t}\n\n\t\tif ( false === $max ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_coupon_invalid_min_max',\n\t\t\t\t'The maximum coupon amount must be a valid positive integer.'\n\t\t\t);\n\t\t}\n\n\t\tif ( $min > $max ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_coupon_invalid_min_max',\n\t\t\t\t'The maximum coupon amount must be an integer that is greater than or equal to the minimum amount.'\n\t\t\t);\n\t\t}\n\n\t\t// Validate discount_type if provided\n\t\t$discount_type = ! empty( $args['discount_type'] ) ? $args['discount_type'] : '';\n\t\tif ( ! empty( $discount_type ) && ! in_array( $discount_type, array( 'fixed_cart', 'percent' ), true ) ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_coupon_invalid_discount_type',\n\t\t\t\t'The discount_type must be either \"fixed_cart\" or \"percent\".'\n\t\t\t);\n\t\t}\n\n\t\t$code        = substr( self::$faker->promotionCode( 1 ), 0, -1 ); // Omit the random digit.\n\t\t$amount      = self::$faker->numberBetween( $min, $max );\n\t\t$coupon_code = sprintf(\n\t\t\t'%s%d',\n\t\t\t$code,\n\t\t\t$amount\n\t\t);\n\n\t\t$props = array(\n\t\t\t'code'   => $coupon_code,\n\t\t\t'amount' => $amount,\n\t\t);\n\n\t\t// Only set discount_type if explicitly provided\n\t\tif ( ! empty( $discount_type ) ) {\n\t\t\t$props['discount_type'] = $discount_type;\n\t\t}\n\n\t\t$coupon = new \\WC_Coupon( $coupon_code );\n\t\t$coupon->set_props( $props );\n\n\t\tif ( $save ) {\n\t\t\t$data_store = WC_Data_Store::load( 'coupon' );\n\t\t\t$data_store->create( $coupon );\n\t\t}\n\n\t\t/**\n\t\t * Action: Coupon generator returned a new coupon.\n\t\t *\n\t\t * @since 1.2.0\n\t\t *\n\t\t * @param \\WC_Coupon $coupon\n\t\t */\n\t\tdo_action( 'smoothgenerator_coupon_generated', $coupon );\n\n\t\treturn $coupon;\n\t}\n\n\t/**\n\t * Create multiple coupons.\n\t *\n\t * @param int   $amount The number of coupons to create.\n\t * @param array $args   Additional args for coupon creation.\n\t *\n\t * @return int[]|\\WP_Error\n\t */\n\tpublic static function batch( $amount, array $args = array() ) {\n\t\t$amount = self::validate_batch_amount( $amount );\n\t\tif ( is_wp_error( $amount ) ) {\n\t\t\treturn $amount;\n\t\t}\n\n\t\t$coupon_ids = array();\n\n\t\tfor ( $i = 1; $i <= $amount; $i ++ ) {\n\t\t\t$coupon = self::generate( true, $args );\n\t\t\tif ( is_wp_error( $coupon ) ) {\n\t\t\t\treturn $coupon;\n\t\t\t}\n\t\t\t$coupon_ids[] = $coupon->get_id();\n\t\t}\n\n\t\treturn $coupon_ids;\n\t}\n\n\t/**\n\t * Get a random existing coupon.\n\t *\n\t * @return \\WC_Coupon|false Coupon object or false if none available.\n\t */\n\tpublic static function get_random() {\n\t\t// Note: Using posts_per_page=-1 loads all coupon IDs into memory for random selection.\n\t\t// For stores with thousands of coupons, consider using direct SQL with RAND() for better performance.\n\t\t// This approach was chosen for consistency with WordPress APIs and to avoid raw SQL queries.\n\t\t$coupon_ids = get_posts(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'shop_coupon',\n\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t'posts_per_page' => -1,\n\t\t\t\t'fields'         => 'ids',\n\t\t\t)\n\t\t);\n\n\t\tif ( empty( $coupon_ids ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$random_coupon_id = $coupon_ids[ array_rand( $coupon_ids ) ];\n\n\t\treturn new \\WC_Coupon( $random_coupon_id );\n\t}\n}\n\n"
  },
  {
    "path": "includes/Generator/Customer.php",
    "content": "<?php\n/**\n * Customer data generation.\n *\n * @package SmoothGenerator\\Classes\n */\n\nnamespace WC\\SmoothGenerator\\Generator;\n\n/**\n * Customer data generator.\n */\nclass Customer extends Generator {\n\t/**\n\t * Return a new customer.\n\t *\n\t * @param bool  $save       Save the object before returning or not.\n\t * @param array $assoc_args Arguments passed via the CLI for additional customization.\n\t *\n\t * @return \\WC_Customer|\\WP_Error Customer object with data populated.\n\t */\n\tpublic static function generate( $save = true, array $assoc_args = array() ) {\n\t\tparent::maybe_initialize_generators();\n\n\t\t$args = filter_var_array(\n\t\t\t$assoc_args,\n\t\t\tarray(\n\t\t\t\t'country' => array(\n\t\t\t\t\t'filter'  => FILTER_VALIDATE_REGEXP,\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'regexp'  => '/^[A-Za-z]{2}$/',\n\t\t\t\t\t\t'default' => '',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'type'    => array(\n\t\t\t\t\t'filter'  => FILTER_VALIDATE_REGEXP,\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'regexp' => '/^(company|person)$/',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\tlist( 'country' => $country, 'type' => $type ) = $args;\n\n\t\t$country = CustomerInfo::get_valid_country_code( $country );\n\t\tif ( is_wp_error( $country ) ) {\n\t\t\treturn $country;\n\t\t}\n\n\t\tif ( ! $type ) {\n\t\t\t$type = self::$faker->randomDigit() < 7 ? 'person' : 'company'; // 70% person, 30% company.\n\t\t}\n\n\t\t$keys_for_address = array( 'email' );\n\n\t\t$customer_data = array(\n\t\t\t'role' => 'customer',\n\t\t);\n\t\tswitch ( $type ) {\n\t\t\tcase 'person':\n\t\t\tdefault:\n\t\t\t\t$customer_data       = array_merge( $customer_data, CustomerInfo::generate_person( $country ) );\n\t\t\t\t$other_customer_data = CustomerInfo::generate_person( $country );\n\t\t\t\t$keys_for_address[]  = 'first_name';\n\t\t\t\t$keys_for_address[]  = 'last_name';\n\t\t\t\tbreak;\n\n\t\t\tcase 'company':\n\t\t\t\t$customer_data       = array_merge( $customer_data, CustomerInfo::generate_company( $country ) );\n\t\t\t\t$other_customer_data = CustomerInfo::generate_company( $country );\n\t\t\t\t$keys_for_address[]  = 'company';\n\t\t\t\tbreak;\n\t\t}\n\n\t\t$customer_data['billing'] = array_merge(\n\t\t\tCustomerInfo::generate_address( $country ),\n\t\t\tarray_intersect_key( $customer_data, array_fill_keys( $keys_for_address, '' ) )\n\t\t);\n\n\t\t$has_shipping = self::$faker->randomDigit() < 5;\n\t\tif ( $has_shipping ) {\n\t\t\t$same_shipping = self::$faker->randomDigit() < 5;\n\t\t\tif ( $same_shipping ) {\n\t\t\t\t$customer_data['shipping'] = $customer_data['billing'];\n\t\t\t} else {\n\t\t\t\t$customer_data['shipping'] = array_merge(\n\t\t\t\t\tCustomerInfo::generate_address( $country ),\n\t\t\t\t\tarray_intersect_key( $other_customer_data, array_fill_keys( $keys_for_address, '' ) )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tunset( $customer_data['company'], $customer_data['shipping']['email'] );\n\n\t\tforeach ( array( 'billing', 'shipping' ) as $address_type ) {\n\t\t\tif ( isset( $customer_data[ $address_type ] ) ) {\n\t\t\t\t$address_data = array_combine(\n\t\t\t\t\tarray_map(\n\t\t\t\t\t\tfn( $line ) => $address_type . '_' . $line,\n\t\t\t\t\t\tarray_keys( $customer_data[ $address_type ] )\n\t\t\t\t\t),\n\t\t\t\t\tarray_values( $customer_data[ $address_type ] )\n\t\t\t\t);\n\n\t\t\t\t$customer_data = array_merge( $customer_data, $address_data );\n\t\t\t\tunset( $customer_data[ $address_type ] );\n\t\t\t}\n\t\t}\n\n\t\t$customer = new \\WC_Customer();\n\t\t$customer->set_props( $customer_data );\n\n\t\tif ( $save ) {\n\t\t\t$customer->save();\n\t\t}\n\n\t\t/**\n\t\t * Action: Customer generator returned a new customer.\n\t\t *\n\t\t * @since 1.2.0\n\t\t *\n\t\t * @param \\WC_Customer $customer\n\t\t */\n\t\tdo_action( 'smoothgenerator_customer_generated', $customer );\n\n\t\treturn $customer;\n\t}\n\n\t/**\n\t * Create multiple customers.\n\t *\n\t * @param int   $amount The number of customers to create.\n\t * @param array $args   Additional args for customer creation.\n\t *\n\t * @return int[]|\\WP_Error\n\t */\n\tpublic static function batch( $amount, array $args = array() ) {\n\t\t$amount = self::validate_batch_amount( $amount );\n\t\tif ( is_wp_error( $amount ) ) {\n\t\t\treturn $amount;\n\t\t}\n\n\t\t$customer_ids = array();\n\n\t\tfor ( $i = 1; $i <= $amount; $i++ ) {\n\t\t\t$customer       = self::generate( true, $args );\n\t\t\tif ( is_wp_error( $customer ) ) {\n\t\t\t\treturn $customer;\n\t\t\t}\n\t\t\t$customer_ids[] = $customer->get_id();\n\t\t}\n\n\t\treturn $customer_ids;\n\t}\n}\n"
  },
  {
    "path": "includes/Generator/CustomerInfo.php",
    "content": "<?php\n\nnamespace WC\\SmoothGenerator\\Generator;\n\n/**\n * Class CustomerInfo.\n *\n * Helper class for generating locale-specific coherent customer test data.\n */\nclass CustomerInfo {\n\t/**\n\t * Get a country code for a country that the store is set to sell to, or validate a given country code.\n\t *\n\t * @param string|null $country_code ISO 3166-1 alpha-2 country code. E.g. US, ES, CN, RU etc.\n\t *\n\t * @return string|\\WP_Error\n\t */\n\tpublic static function get_valid_country_code( ?string $country_code = '' ) {\n\t\t$country_code = !empty( $country_code ) ? strtoupper( $country_code ) : '';\n\n\t\tif ( $country_code && ! WC()->countries->country_exists( $country_code ) ) {\n\t\t\t$country_code = new \\WP_Error(\n\t\t\t\t'smoothgenerator_customer_invalid_country',\n\t\t\t\tsprintf(\n\t\t\t\t\t'No data for a country with country code \"%s\"',\n\t\t\t\t\tesc_html( $country_code )\n\t\t\t\t)\n\t\t\t);\n\t\t} elseif ( ! $country_code ) {\n\t\t\t$valid_countries = WC()->countries->get_allowed_countries();\n\t\t\t$country_code    = array_rand( $valid_countries );\n\t\t}\n\n\t\treturn $country_code;\n\t}\n\n\t/**\n\t * Retrieve locale data for a given country.\n\t *\n\t * @param string string $country_code ISO 3166-1 alpha-2 country code. E.g. US, ES, CN, RU etc.\n\t *\n\t * @return array\n\t */\n\tprotected static function get_country_locale_info( string $country_code = 'en_US' ) {\n\t\t$all_locale_info = include WC()->plugin_path() . '/i18n/locale-info.php';\n\n\t\tif ( ! isset( $all_locale_info[ $country_code ] ) ) {\n\t\t\treturn array();\n\t\t}\n\n\t\treturn $all_locale_info[ $country_code ];\n\t}\n\n\n\t/**\n\t * Get a localized Faker library instance.\n\t *\n\t * @param string $country_code ISO 3166-1 alpha-2 country code. E.g. US, ES, CN, RU etc.\n\t *\n\t * @return \\Faker\\Generator\n\t */\n\tprotected static function get_faker( $country_code = 'en_US' ) {\n\t\t$locale_info    = self::get_country_locale_info( $country_code );\n\t\t$default_locale = ! empty( $locale_info['default_locale'] ) ? $locale_info['default_locale'] : 'en_US';\n\n\t\t$faker = \\Faker\\Factory::create( $default_locale );\n\n\t\treturn $faker;\n\t}\n\n\t/**\n\t * Retrieve the localized instance of a particular provider from within the Faker.\n\t *\n\t * @param \\Faker\\Generator $faker         The current instance of the Faker.\n\t * @param string           $provider_name The name of the provider to retrieve. E.g. 'Person'.\n\t *\n\t * @return \\Faker\\Provider\\Base|null\n\t */\n\tprotected static function get_provider_instance( \\Faker\\Generator $faker, string $provider_name ) {\n\t\t$instance = null;\n\t\tforeach ( $faker->getProviders() as $provider ) {\n\t\t\tif ( str_ends_with( get_class( $provider ), $provider_name ) ) {\n\t\t\t\t$instance = $provider;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn $instance;\n\t}\n\n\t/**\n\t * Generate data for a person, localized for a particular country.\n\t *\n\t * Includes first name, last name, username, email address, and password.\n\t *\n\t * @param string $country_code ISO 3166-1 alpha-2 country code. E.g. US, ES, CN, RU etc.\n\t *\n\t * @return string[]|\\WP_Error\n\t * @throws \\ReflectionException\n\t */\n\tpublic static function generate_person( string $country_code = '' ) {\n\t\t$country_code = self::get_valid_country_code( $country_code );\n\t\tif ( is_wp_error( $country_code ) ) {\n\t\t\treturn $country_code;\n\t\t}\n\n\t\t$faker = self::get_faker( $country_code );\n\n\t\t$first_name = $faker->firstName( $faker->randomElement( array( 'male', 'female' ) ) );\n\t\t$last_name  = $faker->lastName();\n\n\t\tif ( $faker->randomDigit() < 3 ) {\n\t\t\t// 30% chance for no capitalization.\n\t\t\t$first_name = strtolower( $first_name );\n\t\t\t$last_name  = strtolower( $last_name );\n\t\t}\n\n\t\t$person = array(\n\t\t\t'first_name' => $first_name,\n\t\t\t'last_name'  => $last_name,\n\t\t\t'password'   => 'password',\n\t\t);\n\n\t\t// Make sure email and username use the same first and last name that were previously generated.\n\t\t$person_provider    = self::get_provider_instance( $faker, 'Person' );\n\t\t$reflected_provider = new \\ReflectionClass( $person_provider );\n\t\t$orig_fn_male       = $reflected_provider->getStaticPropertyValue( 'firstNameMale', array() );\n\t\t$orig_fn_female     = $reflected_provider->getStaticPropertyValue( 'firstNameFemale', array() );\n\t\t$orig_ln            = $reflected_provider->getStaticPropertyValue( 'lastName', array() );\n\n\t\t$reflected_provider->setStaticPropertyValue( 'firstNameMale', array( $first_name ) );\n\t\t$reflected_provider->setStaticPropertyValue( 'firstNameFemale', array( $first_name ) );\n\t\t$reflected_provider->setStaticPropertyValue( 'lastName', array( $last_name ) );\n\n\t\t$person['display_name'] = $faker->name();\n\n\t\t// Switch Faker to default locale if transliteration fails or there's another issue.\n\t\ttry {\n\t\t\t$faker->safeEmail();\n\t\t\t$faker->userName();\n\t\t} catch ( \\Exception $e ) {\n\t\t\t$faker = self::get_faker();\n\t\t}\n\n\t\tdo {\n\t\t\t$person['email'] = $faker->safeEmail();\n\t\t} while ( email_exists( $person['email'] ) );\n\n\t\tdo {\n\t\t\t$person['username'] = $faker->userName();\n\t\t} while ( username_exists( $person['username'] ) );\n\n\t\t$reflected_provider->setStaticPropertyValue( 'firstNameMale', $orig_fn_male );\n\t\t$reflected_provider->setStaticPropertyValue( 'firstNameFemale', $orig_fn_female );\n\t\t$reflected_provider->setStaticPropertyValue( 'lastName', $orig_ln );\n\n\t\treturn $person;\n\t}\n\n\t/**\n\t * Generate data for a company, localized for a particular country.\n\t *\n\t * Includes company name, username, email address, and password.\n\t *\n\t * @param string $country_code ISO 3166-1 alpha-2 country code. E.g. US, ES, CN, RU etc.\n\t *\n\t * @return string[]|\\WP_Error\n\t */\n\tpublic static function generate_company( string $country_code = '' ) {\n\t\t$country_code = self::get_valid_country_code( $country_code );\n\t\tif ( is_wp_error( $country_code ) ) {\n\t\t\treturn $country_code;\n\t\t}\n\n\t\t$faker = self::get_faker( $country_code );\n\n\t\t$last_names = array();\n\t\tfor ( $i = 0; $i < 3; $i++ ) {\n\t\t\ttry {\n\t\t\t\t$last_names[] = $faker->unique()->lastName();\n\t\t\t} catch ( \\OverflowException $e ) {\n\t\t\t\t$last_names[] = $faker->unique( true )->lastName();\n\t\t\t}\n\t\t}\n\n\t\tif ( $faker->randomDigit() < 3 ) {\n\t\t\t// 30% chance for no capitalization.\n\t\t\t$last_names = array_map( 'strtolower', $last_names );\n\t\t}\n\n\t\t// Make sure all the company-related strings draw from the same set of last names that were previously generated.\n\t\t$person_provider    = self::get_provider_instance( $faker, 'Person' );\n\t\t$reflected_provider = new \\ReflectionClass( $person_provider );\n\t\t$orig_ln            = $reflected_provider->getStaticPropertyValue( 'lastName', array() );\n\n\t\t$reflected_provider->setStaticPropertyValue( 'lastName', $last_names );\n\n\t\t$company = array(\n\t\t\t'company'  => $faker->company(),\n\t\t\t'password' => 'password',\n\t\t);\n\n\t\t$company['display_name'] = $company['company'];\n\n\t\t$reflected_provider->setStaticPropertyValue( 'lastName', array( $faker->randomElement( $last_names ) ) );\n\n\t\t// Make sure a unique email and username are used.\n\t\tdo {\n\t\t\ttry {\n\t\t\t\t$company['email'] = $faker->companyEmail();\n\t\t\t} catch ( \\Exception $e ) {\n\t\t\t\t$default_faker    = self::get_faker();\n\t\t\t\t$company['email'] = $default_faker->email();\n\t\t\t}\n\t\t} while ( email_exists( $company['email'] ) );\n\n\t\tdo {\n\t\t\ttry {\n\t\t\t\t$company['username'] = $faker->domainWord() . $faker->optional()->randomNumber( 2 );\n\t\t\t} catch ( \\Exception $e ) {\n\t\t\t\t$default_faker       = self::get_faker();\n\t\t\t\t$company['username'] = $default_faker->userName();\n\t\t\t}\n\t\t} while ( username_exists( $company['username'] ) || strlen( $company['username'] ) < 3 );\n\n\t\t$reflected_provider->setStaticPropertyValue( 'lastName', $orig_ln );\n\n\t\treturn $company;\n\t}\n\n\t/**\n\t * Generate address data, localized for a particular country.\n\t *\n\t * @param string $country_code ISO 3166-1 alpha-2 country code. E.g. US, ES, CN, RU etc.\n\t *\n\t * @return string[]|\\WP_Error\n\t */\n\tpublic static function generate_address( string $country_code = '' ) {\n\t\t$country_code = self::get_valid_country_code( $country_code );\n\t\tif ( is_wp_error( $country_code ) ) {\n\t\t\treturn $country_code;\n\t\t}\n\n\t\t$faker = self::get_faker( $country_code );\n\n\t\t$address = array(\n\t\t\t'address1' => '',\n\t\t\t'city'     => '',\n\t\t\t'state'    => '',\n\t\t\t'postcode' => '',\n\t\t\t'country'  => '',\n\t\t\t'phone'    => '',\n\t\t);\n\n\t\t$exceptions = WC()->countries->get_country_locale();\n\t\tforeach ( array_keys( $address ) as $line ) {\n\t\t\tif ( isset( $exceptions[ $country_code ][ $line ]['hidden'] ) && true === $exceptions[ $country_code ][ $line ]['hidden'] ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( isset( $exceptions[ $country_code ][ $line ]['required'] ) && false === $exceptions[ $country_code ][ $line ]['required'] ) {\n\t\t\t\t// 50% chance to skip if it's not required.\n\t\t\t\tif ( $faker->randomDigit() < 5 ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tswitch ( $line ) {\n\t\t\t\tcase 'address1':\n\t\t\t\t\t$address[ $line ] = $faker->streetAddress();\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'city':\n\t\t\t\t\t$address[ $line ] = $faker->city();\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'state':\n\t\t\t\t\t$states           = WC()->countries->get_states( $country_code );\n\t\t\t\t\tif ( is_array( $states ) ) {\n\t\t\t\t\t\t$address[ $line ] = $faker->randomElement( array_keys( $states ) );\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'postcode':\n\t\t\t\t\t$address[ $line ] = $faker->postcode();\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'country':\n\t\t\t\t\t$address[ $line ] = $country_code;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'phone':\n\t\t\t\t\t$address[ $line ] = $faker->phoneNumber();\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn $address;\n\t}\n}\n"
  },
  {
    "path": "includes/Generator/Generator.php",
    "content": "<?php\n/**\n * Abstract Generator class\n *\n * @package SmoothGenerator\\Abstracts\n */\n\nnamespace WC\\SmoothGenerator\\Generator;\n\n/**\n * Data generator base class.\n */\nabstract class Generator {\n\t/**\n\t * Maximum number of objects that can be generated in one batch.\n\t */\n\tconst MAX_BATCH_SIZE = 100;\n\n\t/**\n\t * Dimension, in pixels, of generated images.\n\t */\n\tconst IMAGE_SIZE = 700;\n\n\t/**\n\t * Are we ready to generate objects?\n\t *\n\t * @var bool\n\t */\n\tprotected static $ready = false;\n\n\t/**\n\t * Holds the Faker factory object.\n\t *\n\t * @var \\Faker\\Generator Factory object.\n\t */\n\tprotected static $faker;\n\n\t/**\n\t * Caches term IDs.\n\t *\n\t * @deprecated\n\t *\n\t * @var array Array of IDs.\n\t */\n\tprotected static $term_ids;\n\n\t/**\n\t * Holds array of generated images to assign to products.\n\t *\n\t * @var array Array of image attachment IDs.\n\t */\n\tprotected static $images = array();\n\n\t/**\n\t * Return a new object of this object type.\n\t *\n\t * @param bool $save Save the object before returning or not.\n\t * @return array\n\t */\n\tabstract public static function generate( $save = true );\n\n\t/**\n\t * Create multiple objects.\n\t *\n\t * @param int   $amount Number of objects to create.\n\t * @param array $args   Additional args for object creation.\n\t *\n\t * @return int[]|\\WP_Error An array of IDs of created objects on success.\n\t */\n\t// TODO normalize the signature of this method in all generator classes so we can add this to the contract.\n\t//abstract public static function batch( $amount, array $args = array() );\n\n\t/**\n\t * Get ready to generate objects.\n\t *\n\t * This can be run from any generator, but it applies to all generators.\n\t *\n\t * @return void\n\t */\n\tprotected static function maybe_initialize_generators() {\n\t\tif ( true !== self::$ready ) {\n\t\t\tself::init_faker();\n\t\t\tself::disable_emails();\n\n\t\t\t// Set this to avoid notices as when you run via WP-CLI SERVER vars are not set, order emails uses this variable.\n\t\t\tif ( ! isset( $_SERVER['SERVER_NAME'] ) ) {\n\t\t\t\t$_SERVER['SERVER_NAME'] = 'localhost';\n\t\t\t}\n\t\t}\n\n\t\tself::$ready = true;\n\t}\n\n\t/**\n\t * Create and store an instance of the Faker library.\n\t */\n\tprotected static function init_faker() {\n\t\tif ( ! self::$faker ) {\n\t\t\tself::$faker = \\Faker\\Factory::create( 'en_US' );\n\t\t\tself::$faker->addProvider( new \\Bezhanov\\Faker\\Provider\\Commerce( self::$faker ) );\n\t\t}\n\t}\n\n\t/**\n\t * Disable sending WooCommerce emails when generating objects.\n\t *\n\t * This needs to run as late in the request as possible so that the callbacks we want to remove\n\t * have actually been added.\n\t *\n\t * @return void\n\t */\n\tpublic static function disable_emails() {\n\t\t$email_actions = array(\n\t\t\t// Customer emails.\n\t\t\t'woocommerce_new_customer_note',\n\t\t\t'woocommerce_created_customer',\n\t\t\t// Order emails.\n\t\t\t'woocommerce_order_status_pending_to_processing',\n\t\t\t'woocommerce_order_status_pending_to_completed',\n\t\t\t'woocommerce_order_status_processing_to_cancelled',\n\t\t\t'woocommerce_order_status_pending_to_failed',\n\t\t\t'woocommerce_order_status_pending_to_on-hold',\n\t\t\t'woocommerce_order_status_failed_to_processing',\n\t\t\t'woocommerce_order_status_failed_to_completed',\n\t\t\t'woocommerce_order_status_failed_to_on-hold',\n\t\t\t'woocommerce_order_status_cancelled_to_processing',\n\t\t\t'woocommerce_order_status_cancelled_to_completed',\n\t\t\t'woocommerce_order_status_cancelled_to_on-hold',\n\t\t\t'woocommerce_order_status_on-hold_to_processing',\n\t\t\t'woocommerce_order_status_on-hold_to_cancelled',\n\t\t\t'woocommerce_order_status_on-hold_to_failed',\n\t\t\t'woocommerce_order_status_completed',\n\t\t\t'woocommerce_order_status_failed',\n\t\t\t'woocommerce_order_fully_refunded',\n\t\t\t'woocommerce_order_partially_refunded',\n\t\t\t// Product emails.\n\t\t\t'woocommerce_low_stock',\n\t\t\t'woocommerce_no_stock',\n\t\t\t'woocommerce_product_on_backorder',\n\t\t);\n\n\t\tforeach ( $email_actions as $action ) {\n\t\t\tremove_action( $action, array( 'WC_Emails', 'send_transactional_email' ) );\n\t\t}\n\n\t\tif ( ! has_action( 'woocommerce_allow_send_queued_transactional_email', '__return_false' ) ) {\n\t\t\tadd_action( 'woocommerce_allow_send_queued_transactional_email', '__return_false' );\n\t\t}\n\t}\n\n\t/**\n\t * Validate the value of the amount input for a batch command.\n\t *\n\t * @param int $amount The number of items to create in a batch.\n\t *\n\t * @return mixed|\\WP_Error\n\t */\n\tprotected static function validate_batch_amount( $amount ) {\n\t\t$amount = filter_var(\n\t\t\t$amount,\n\t\t\tFILTER_VALIDATE_INT,\n\t\t\tarray(\n\t\t\t\t'options' => array(\n\t\t\t\t\t'min_range' => 1,\n\t\t\t\t\t'max_range' => static::MAX_BATCH_SIZE,\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\tif ( false === $amount ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_batch_invalid_amount',\n\t\t\t\tsprintf(\n\t\t\t\t\t'Amount must be a number between 1 and %d.',\n\t\t\t\t\tstatic::MAX_BATCH_SIZE\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn $amount;\n\t}\n\n\t/**\n\t * Get random term ids.\n\t *\n\t * @deprecated Use Product::get_term_ids instead.\n\t *\n\t * @param int    $limit Number of term IDs to get.\n\t * @param string $taxonomy Taxonomy name.\n\t * @param string $name Product name to extract terms from.\n\t *\n\t * @return array\n\t */\n\tprotected static function generate_term_ids( $limit, $taxonomy, $name = '' ) {\n\t\t_deprecated_function( __METHOD__, '1.2.2', 'Product::get_term_ids' );\n\n\t\tself::init_faker();\n\n\t\t$term_ids = array();\n\n\t\tif ( ! $limit ) {\n\t\t\treturn $term_ids;\n\t\t}\n\n\t\t$words       = str_word_count( $name, 1 );\n\t\t$extra_terms = str_word_count( self::$faker->department( $limit ), 1 );\n\t\t$words       = array_merge( $words, $extra_terms );\n\n\t\tif ( 'product_cat' === $taxonomy ) {\n\t\t\t$terms = array_slice( $words, 1 );\n\t\t} else {\n\t\t\t$terms = array_merge( self::$faker->words( $limit ), array( strtolower( $words[0] ) ) );\n\t\t}\n\n\t\tforeach ( $terms as $term ) {\n\t\t\tif ( isset( self::$term_ids[ $taxonomy ], self::$term_ids[ $taxonomy ][ $term ] ) ) {\n\t\t\t\t$term_id    = self::$term_ids[ $taxonomy ][ $term ];\n\t\t\t\t$term_ids[] = $term_id;\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$term_id = 0;\n\t\t\t$args    = array(\n\t\t\t\t'taxonomy' => $taxonomy,\n\t\t\t\t'name'     => $term,\n\t\t\t);\n\n\t\t\t$existing = get_terms( $args );\n\n\t\t\tif ( $existing && count( $existing ) && ! is_wp_error( $existing ) ) {\n\t\t\t\t$term_id = $existing[0]->term_id;\n\t\t\t} else {\n\t\t\t\t$term_ob = wp_insert_term( $term, $taxonomy, $args );\n\n\t\t\t\tif ( $term_ob && ! is_wp_error( $term_ob ) ) {\n\t\t\t\t\t$term_id = $term_ob['term_id'];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( $term_id ) {\n\t\t\t\t$term_ids[]                           = $term_id;\n\t\t\t\tself::$term_ids[ $taxonomy ][ $term ] = $term_id;\n\t\t\t}\n\t\t}\n\n\t\treturn $term_ids;\n\t}\n\n\t/**\n\t * Create/retrieve a set of random images to assign to products.\n\t *\n\t * @param integer $amount Number of images required.\n\t */\n\tpublic static function seed_images( $amount = 10 ) {\n\t\tself::$images = get_posts(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'attachment',\n\t\t\t\t'fields'         => 'ids',\n\t\t\t\t'parent'         => 0,\n\t\t\t\t'posts_per_page' => $amount,\n\t\t\t\t'exclude'        => get_option( 'woocommerce_placeholder_image', 0 ),\n\t\t\t)\n\t\t);\n\n\t\t$found_count = count( self::$images );\n\n\t\tfor ( $i = 1; $i <= ( $amount - $found_count ); $i++ ) {\n\t\t\tself::$images[] = self::generate_image();\n\t\t}\n\t}\n\n\t/**\n\t * Get an image at random from our seeded data.\n\t *\n\t * @return int\n\t */\n\tprotected static function get_image() {\n\t\tif ( ! self::$images ) {\n\t\t\tself::seed_images();\n\t\t}\n\t\treturn self::$images[ array_rand( self::$images ) ];\n\t}\n\n\t/**\n\t * Generate and upload a random image, or choose an existing attachment.\n\t *\n\t * @param string $seed Seed for image generation.\n\t * @return int The attachment id of the image (0 on failure).\n\t */\n\tprotected static function generate_image( $seed = '' ) {\n\t\tself::init_faker();\n\n\t\t$attachment_id = 0;\n\n\t\tif ( ! $seed ) {\n\t\t\t$seed = self::$faker->word();\n\t\t}\n\n\t\t$seed = sanitize_key( $seed );\n\t\t$icon = new \\Jdenticon\\Identicon();\n\t\t$icon->setValue( $seed );\n\t\t$icon->setSize( self::IMAGE_SIZE );\n\n\t\t$image = imagecreatefromstring( @$icon->getImageData() ); // phpcs:ignore\n\t\tob_start();\n\t\timagepng( $image );\n\t\t$file = ob_get_clean();\n\t\t// Unset image to free memory early. imagedestroy() has no effect since PHP 8.0 and is deprecated in PHP 8.5.\n\t\tunset( $image );\n\t\t$upload = wp_upload_bits( 'img-' . $seed . '.png', null, $file );\n\n\t\tif ( empty( $upload['error'] ) ) {\n\t\t\t$attachment_id = (int) wp_insert_attachment(\n\t\t\t\tarray(\n\t\t\t\t\t'post_title'     => 'img-' . $seed . '.png',\n\t\t\t\t\t'post_mime_type' => $upload['type'],\n\t\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t\t'post_content'   => '',\n\t\t\t\t),\n\t\t\t\t$upload['file']\n\t\t\t);\n\t\t}\n\n\t\tif ( $attachment_id ) {\n\t\t\tif ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {\n\t\t\t\tinclude_once ABSPATH . 'wp-admin/includes/image.php';\n\t\t\t}\n\t\t\twp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );\n\t\t}\n\n\t\treturn $attachment_id;\n\t}\n\n\t/**\n\t * Get a random value from an array based on weight.\n\t * Taken from https://stackoverflow.com/questions/445235/generating-random-results-by-weight-in-php\n\t *\n\t * @param array $weighted_values Array of value => weight options.\n\t * @return mixed\n\t */\n\tprotected static function random_weighted_element( array $weighted_values ) {\n\t\t$rand = wp_rand( 1, (int) array_sum( $weighted_values ) );\n\n\t\tforeach ( $weighted_values as $key => $value ) {\n\t\t\t$rand -= $value;\n\t\t\tif ( $rand <= 0 ) {\n\t\t\t\treturn $key;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/Generator/Order.php",
    "content": "<?php\n/**\n * Order data generation.\n *\n * @package SmoothGenerator\\Classes\n */\n\nnamespace WC\\SmoothGenerator\\Generator;\n\n/**\n * Order data generator.\n */\nclass Order extends Generator {\n\n\t/**\n\t * Probability (percentage) that a partial refund will receive a second refund.\n\t */\n\tconst SECOND_REFUND_PROBABILITY = 25;\n\n\t/**\n\t * Maximum ratio of order total that can be refunded in a partial refund.\n\t * Ensures partial refunds don't exceed 50% of order total.\n\t */\n\tconst MAX_PARTIAL_REFUND_RATIO = 0.5;\n\n\t/**\n\t * Maximum days after order completion for first refund (2 months).\n\t */\n\tconst FIRST_REFUND_MAX_DAYS = 60;\n\n\t/**\n\t * Maximum days after first refund for second refund (1 month).\n\t */\n\tconst SECOND_REFUND_MAX_DAYS = 30;\n\n\t/**\n\t * Refund type constants for memory-efficient batch operations.\n\t */\n\tconst REFUND_TYPE_NONE = 0;\n\tconst REFUND_TYPE_FULL = 1;\n\tconst REFUND_TYPE_PARTIAL = 2;\n\tconst REFUND_TYPE_MULTI = 3;\n\n\t/**\n\t * Refund distribution ratios for batch generation with exact ratios.\n\t * When generating refunds in batch mode:\n\t * - 50% will be full refunds\n\t * - 25% will be single partial refunds\n\t * - 25% will be multi-partial refunds (two partial refunds)\n\t */\n\tconst REFUND_DISTRIBUTION_FULL_RATIO = 0.5;\n\tconst REFUND_DISTRIBUTION_PARTIAL_RATIO = 0.25;\n\n\t/**\n\t * Return a new order.\n\t *\n\t * @param bool        $save Save the object before returning or not.\n\t * @param array       $assoc_args Arguments passed via the CLI for additional customization.\n\t * @param string|null $date Optional date string (Y-m-d) to use for order creation. If not provided, will be generated.\n\t * @param bool|null   $include_coupon Optional flag to include coupon. If null, will be determined based on coupon-ratio.\n\t * @param int|null    $refund_type Optional refund type constant. If null, will be determined based on refund-ratio.\n\t * @return \\WC_Order|false Order object with data populated or false when failed.\n\t */\n\tpublic static function generate( $save = true, $assoc_args = array(), $date = null, $include_coupon = null, $refund_type = null ) {\n\t\tparent::maybe_initialize_generators();\n\n\t\t$order    = new \\WC_Order();\n\t\t$customer = self::get_customer();\n\t\tif ( ! $customer instanceof \\WC_Customer ) {\n\t\t\terror_log( 'Order generation failed: Could not generate or retrieve customer' );\n\t\t\treturn false;\n\t\t}\n\t\t$products = self::get_random_products( 1, 10 );\n\n\t\tif ( empty( $products ) ) {\n\t\t\terror_log( 'Order generation failed: No products available to add to order' );\n\t\t\treturn false;\n\t\t}\n\n\t\tforeach ( $products as $product ) {\n\t\t\t$quantity = self::$faker->numberBetween( 1, 10 );\n\t\t\t$order->add_product( $product, $quantity );\n\t\t}\n\n\t\t$order->set_customer_id( $customer->get_id() );\n\t\t$order->set_created_via( 'smooth-generator' );\n\t\t$order->set_currency( get_woocommerce_currency() );\n\t\t$order->set_billing_first_name( $customer->get_billing_first_name() );\n\t\t$order->set_billing_last_name( $customer->get_billing_last_name() );\n\t\t$order->set_billing_address_1( $customer->get_billing_address_1() );\n\t\t$order->set_billing_address_2( $customer->get_billing_address_2() );\n\t\t$order->set_billing_email( $customer->get_billing_email() );\n\t\t$order->set_billing_phone( $customer->get_billing_phone() );\n\t\t$order->set_billing_city( $customer->get_billing_city() );\n\t\t$order->set_billing_postcode( $customer->get_billing_postcode() );\n\t\t$order->set_billing_state( $customer->get_billing_state() );\n\t\t$order->set_billing_country( $customer->get_billing_country() );\n\t\t$order->set_billing_company( $customer->get_billing_company() );\n\t\t$order->set_shipping_first_name( $customer->get_shipping_first_name() );\n\t\t$order->set_shipping_last_name( $customer->get_shipping_last_name() );\n\t\t$order->set_shipping_address_1( $customer->get_shipping_address_1() );\n\t\t$order->set_shipping_address_2( $customer->get_shipping_address_2() );\n\t\t$order->set_shipping_city( $customer->get_shipping_city() );\n\t\t$order->set_shipping_postcode( $customer->get_shipping_postcode() );\n\t\t$order->set_shipping_state( $customer->get_shipping_state() );\n\t\t$order->set_shipping_country( $customer->get_shipping_country() );\n\t\t$order->set_shipping_company( $customer->get_shipping_company() );\n\n\t\t// 20% chance\n\t\tif ( rand( 0, 100 ) <= 20 ) {\n\t\t\t$country_code = $order->get_shipping_country();\n\n\t\t\t$calculate_tax_for = array(\n\t\t\t\t'country' => $country_code,\n\t\t\t\t'state' => '',\n\t\t\t\t'postcode' => '',\n\t\t\t\t'city' => '',\n\t\t\t);\n\n\t\t\t$fee = new \\WC_Order_Item_Fee();\n\t\t\t$randomAmount = self::$faker->randomFloat( 2, 0.05, 100 );\n\n\t\t\t$fee->set_name( 'Extra Fee' );\n\t\t\t$fee->set_amount( $randomAmount );\n\t\t\t$fee->set_tax_class( '' );\n\t\t\t$fee->set_tax_status( 'taxable' );\n\t\t\t$fee->set_total( $randomAmount );\n\t\t\t$fee->calculate_taxes( $calculate_tax_for );\n\t\t\t$order->add_item( $fee );\n\t\t}\n\t\t$status = self::get_status( $assoc_args );\n\t\t$order->set_status( $status );\n\t\t$order->calculate_totals( true );\n\n\t\t// Use provided date or generate one\n\t\tif ( null === $date ) {\n\t\t\t$date = self::get_date_created( $assoc_args );\n\t\t}\n\t\t$date .= ' ' . wp_rand( 0, 23 ) . ':00:00';\n\n\t\t$order->set_date_created( $date );\n\n\t\t// Coupon parameter precedence:\n\t\t// 1. Batch mode flag (from generate_coupon_flags) - takes highest priority\n\t\t// 2. Legacy --coupons flag - used if batch flag not provided\n\t\t// 3. Probabilistic --coupon-ratio - used if neither batch nor legacy flags are set\n\n\t\t// Handle legacy --coupons flag (only if not provided from batch mode)\n\t\tif ( null === $include_coupon ) {\n\t\t\t$include_coupon = ! empty( $assoc_args['coupons'] );\n\t\t}\n\n\t\t// Handle --coupon-ratio parameter\n\t\tif ( isset( $assoc_args['coupon-ratio'] ) && null === $include_coupon ) {\n\t\t\t// Use probabilistic approach for single order generation or when flag not provided\n\t\t\t$coupon_ratio = floatval( $assoc_args['coupon-ratio'] );\n\n\t\t\t// Validate ratio is between 0.0 and 1.0\n\t\t\tif ( $coupon_ratio < 0.0 || $coupon_ratio > 1.0 ) {\n\t\t\t\t$coupon_ratio = max( 0.0, min( 1.0, $coupon_ratio ) );\n\t\t\t}\n\n\t\t\t// Apply coupon based on ratio\n\t\t\tif ( $coupon_ratio >= 1.0 ) {\n\t\t\t\t$include_coupon = true;\n\t\t\t} elseif ( $coupon_ratio > 0 && wp_rand( 1, 100 ) <= ( $coupon_ratio * 100 ) ) {\n\t\t\t\t$include_coupon = true;\n\t\t\t} else {\n\t\t\t\t$include_coupon = false;\n\t\t\t}\n\t\t}\n\n\t\tif ( $include_coupon ) {\n\t\t\t$coupon = self::get_or_create_coupon();\n\t\t\tif ( $coupon ) {\n\t\t\t\t$apply_result = $order->apply_coupon( $coupon );\n\t\t\t\tif ( is_wp_error( $apply_result ) ) {\n\t\t\t\t\terror_log( 'Coupon application failed: ' . $apply_result->get_error_message() . ' (Coupon: ' . $coupon->get_code() . ')' );\n\t\t\t\t} else {\n\t\t\t\t\t// Recalculate totals after applying coupon\n\t\t\t\t\t$order->calculate_totals( true );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Orders created before 2024-01-09 represents orders created before the attribution feature was added.\n\t\tif ( ! ( strtotime( $date ) < strtotime( '2024-01-09' ) ) ) {\n\t\t\t$attribution_result = OrderAttribution::add_order_attribution_meta( $order, $assoc_args );\n\t\t\tif ( $attribution_result && is_wp_error( $attribution_result ) ) {\n\t\t\t\terror_log( 'Order attribution meta addition failed: ' . $attribution_result->get_error_message() );\n\t\t\t}\n\t\t}\n\n\t\t// Set paid and completed dates based on order status.\n\t\tif ( 'completed' === $status || 'processing' === $status ) {\n\t\t\t// Add random 0 to 36 hours to creation date.\n\t\t\t$date_paid = date( 'Y-m-d H:i:s', strtotime( $date ) + ( wp_rand( 0, 36 ) * HOUR_IN_SECONDS ) );\n\t\t\t$order->set_date_paid( $date_paid );\n\t\t\tif ( 'completed' === $status ) {\n\t\t\t\t// Add random 0 to 36 hours to paid date.\n\t\t\t\t$date_completed = date( 'Y-m-d H:i:s', strtotime( $date_paid ) + ( wp_rand( 0, 36 ) * HOUR_IN_SECONDS ) );\n\t\t\t\t$order->set_date_completed( $date_completed );\n\t\t\t}\n\t\t}\n\n\t\tif ( $save ) {\n\t\t\t$save_result = $order->save();\n\t\t\tif ( is_wp_error( $save_result ) ) {\n\t\t\t\terror_log( 'Order save failed: ' . $save_result->get_error_message() );\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Handle --refund-ratio parameter for completed orders\n\t\t\tif ( isset( $assoc_args['refund-ratio'] ) && 'completed' === $status ) {\n\t\t\t\t// Use provided refund type or determine probabilistically\n\t\t\t\tif ( null === $refund_type ) {\n\t\t\t\t\t$refund_ratio = floatval( $assoc_args['refund-ratio'] );\n\n\t\t\t\t\t// Validate ratio is between 0.0 and 1.0\n\t\t\t\t\tif ( $refund_ratio < 0.0 || $refund_ratio > 1.0 ) {\n\t\t\t\t\t\t$refund_ratio = max( 0.0, min( 1.0, $refund_ratio ) );\n\t\t\t\t\t}\n\n\t\t\t\t\t$refund_type = self::REFUND_TYPE_NONE;\n\t\t\t\t\tif ( $refund_ratio >= 1.0 ) {\n\t\t\t\t\t\t// Always refund if ratio is 1.0 or higher\n\t\t\t\t\t\t$refund_type = self::REFUND_TYPE_FULL;\n\t\t\t\t\t} elseif ( $refund_ratio > 0 && wp_rand( 1, 100 ) <= ( $refund_ratio * 100 ) ) {\n\t\t\t\t\t\t// Use random chance for ratios between 0 and 1\n\t\t\t\t\t\t// Split evenly between full and partial\n\t\t\t\t\t\t$refund_type = wp_rand( 0, 1 ) ? self::REFUND_TYPE_FULL : self::REFUND_TYPE_PARTIAL;\n\n\t\t\t\t\t\t// 25% chance for multi-partial\n\t\t\t\t\t\tif ( self::REFUND_TYPE_PARTIAL === $refund_type && wp_rand( 1, 100 ) <= self::SECOND_REFUND_PROBABILITY ) {\n\t\t\t\t\t\t\t$refund_type = self::REFUND_TYPE_MULTI;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Process refund based on type\n\t\t\t\tif ( self::REFUND_TYPE_FULL === $refund_type ) {\n\t\t\t\t\tself::create_refund( $order, false, null, true ); // Explicitly full\n\t\t\t\t} elseif ( self::REFUND_TYPE_PARTIAL === $refund_type ) {\n\t\t\t\t\tself::create_refund( $order, true, null, false ); // Explicitly partial\n\t\t\t\t} elseif ( self::REFUND_TYPE_MULTI === $refund_type ) {\n\t\t\t\t\t$first_refund = self::create_refund( $order, true, null, false ); // Explicitly partial\n\t\t\t\t\tif ( $first_refund && is_object( $first_refund ) ) {\n\t\t\t\t\t\tself::create_refund( $order, true, $first_refund, false ); // Explicitly partial\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Action: Order generator returned a new order.\n\t\t *\n\t\t * @since 1.2.0\n\t\t *\n\t\t * @param \\WC_Order $order\n\t\t */\n\t\tdo_action( 'smoothgenerator_order_generated', $order );\n\n\t\treturn $order;\n\t}\n\n\t/**\n\t * Create multiple orders.\n\t *\n\t * @param int    $amount   The number of orders to create.\n\t * @param array  $args     Additional args for order creation.\n\t *\n\t * @return int[]|\\WP_Error\n\t */\n\tpublic static function batch( $amount, array $args = array() ) {\n\t\t$amount = self::validate_batch_amount( $amount );\n\t\tif ( is_wp_error( $amount ) ) {\n\t\t\terror_log( 'Batch generation failed: ' . $amount->get_error_message() );\n\t\t\treturn $amount;\n\t\t}\n\n\t\t// Initialize dynamic counters for exact ratio distribution (O(1) memory)\n\t\t// Using \"selection without replacement\" algorithm for exact counts\n\t\t$coupons_remaining = 0;\n\t\tif ( isset( $args['coupon-ratio'] ) ) {\n\t\t\t$coupon_ratio = floatval( $args['coupon-ratio'] );\n\t\t\t$coupon_ratio = max( 0.0, min( 1.0, $coupon_ratio ) );\n\t\t\t$coupons_remaining = (int) round( $amount * $coupon_ratio );\n\t\t}\n\n\t\t// Initialize refund type counters for weighted selection without replacement\n\t\t$full_remaining = 0;\n\t\t$partial_remaining = 0;\n\t\t$multi_remaining = 0;\n\t\tif ( isset( $args['refund-ratio'] ) && 'completed' === ( $args['status'] ?? '' ) ) {\n\t\t\t$refund_ratio = floatval( $args['refund-ratio'] );\n\t\t\t$refund_ratio = max( 0.0, min( 1.0, $refund_ratio ) );\n\n\t\t\t$total_refunds = (int) round( $amount * $refund_ratio );\n\n\t\t\t// Split using floor to avoid over-allocation, remainder goes to multi\n\t\t\t$full_remaining = (int) floor( $total_refunds * self::REFUND_DISTRIBUTION_FULL_RATIO );\n\t\t\t$partial_remaining = (int) floor( $total_refunds * self::REFUND_DISTRIBUTION_PARTIAL_RATIO );\n\t\t\t$multi_remaining = $total_refunds - $full_remaining - $partial_remaining;\n\t\t}\n\n\t\t// Pre-generate dates if date-start is provided\n\t\t// This ensures chronological order: lower order IDs = earlier dates\n\t\t$dates = null;\n\t\tif ( ! empty( $args['date-start'] ) ) {\n\t\t\t$dates = self::generate_batch_dates( $amount, $args );\n\t\t}\n\n\t\t$order_ids = array();\n\t\t$orders_remaining = $amount;\n\n\t\tfor ( $i = 1; $i <= $amount; $i ++ ) {\n\t\t\t// Use pre-generated date if available, otherwise pass null to generate one\n\t\t\t$date = ( null !== $dates && ! empty( $dates ) ) ? array_shift( $dates ) : null;\n\n\t\t\t// Use selection without replacement for exact coupon distribution\n\t\t\t$include_coupon = null;\n\t\t\tif ( isset( $args['coupon-ratio'] ) ) {\n\t\t\t\t// Probability = remaining_coupons / remaining_orders\n\t\t\t\t// Guarantees exact count while maintaining random distribution\n\t\t\t\t$include_coupon = ( wp_rand( 1, $orders_remaining ) <= $coupons_remaining );\n\t\t\t\tif ( $include_coupon ) {\n\t\t\t\t\t$coupons_remaining--;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Use weighted selection without replacement for exact refund distribution\n\t\t\t$refund_type = null;\n\t\t\tif ( isset( $args['refund-ratio'] ) && 'completed' === ( $args['status'] ?? '' ) ) {\n\t\t\t\t$total_refund_remaining = $full_remaining + $partial_remaining + $multi_remaining;\n\n\t\t\t\tif ( $total_refund_remaining > 0 && wp_rand( 1, $orders_remaining ) <= $total_refund_remaining ) {\n\t\t\t\t\t// This order gets a refund, decide which type using weighted selection\n\t\t\t\t\t// Store thresholds before decrementing\n\t\t\t\t\t$full_threshold = $full_remaining;\n\t\t\t\t\t$partial_threshold = $full_remaining + $partial_remaining;\n\t\t\t\t\t$rand = wp_rand( 1, $total_refund_remaining );\n\n\t\t\t\t\tif ( $rand <= $full_threshold ) {\n\t\t\t\t\t\t$refund_type = self::REFUND_TYPE_FULL;\n\t\t\t\t\t\t$full_remaining--;\n\t\t\t\t\t} elseif ( $rand <= $partial_threshold ) {\n\t\t\t\t\t\t$refund_type = self::REFUND_TYPE_PARTIAL;\n\t\t\t\t\t\t$partial_remaining--;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$refund_type = self::REFUND_TYPE_MULTI;\n\t\t\t\t\t\t$multi_remaining--;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t$refund_type = self::REFUND_TYPE_NONE;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$orders_remaining--;\n\n\t\t\t$order = self::generate( true, $args, $date, $include_coupon, $refund_type );\n\t\t\tif ( ! $order instanceof \\WC_Order ) {\n\t\t\t\terror_log( \"Batch generation failed: Order {$i} of {$amount} could not be generated\" );\n\t\t\t\t// Restore counters since order generation failed\n\t\t\t\t$orders_remaining++;\n\t\t\t\tif ( $include_coupon && isset( $args['coupon-ratio'] ) ) {\n\t\t\t\t\t$coupons_remaining++;\n\t\t\t\t}\n\t\t\t\tif ( isset( $args['refund-ratio'] ) && 'completed' === ( $args['status'] ?? '' ) && null !== $refund_type ) {\n\t\t\t\t\tif ( self::REFUND_TYPE_FULL === $refund_type ) {\n\t\t\t\t\t\t$full_remaining++;\n\t\t\t\t\t} elseif ( self::REFUND_TYPE_PARTIAL === $refund_type ) {\n\t\t\t\t\t\t$partial_remaining++;\n\t\t\t\t\t} elseif ( self::REFUND_TYPE_MULTI === $refund_type ) {\n\t\t\t\t\t\t$multi_remaining++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$order_ids[] = $order->get_id();\n\t\t}\n\n\t\treturn $order_ids;\n\t}\n\n\t/**\n\t * Return a new customer.\n\t *\n\t * @return \\WC_Customer Customer object with data populated.\n\t */\n\tpublic static function get_customer() {\n\t\tglobal $wpdb;\n\n\t\t$guest    = (bool) wp_rand( 0, 1 );\n\t\t$existing = (bool) wp_rand( 0, 1 );\n\n\t\tif ( $existing ) {\n\t\t\t$total_users = (int) $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->users}\" );\n\t\t\t$offset      = wp_rand( 0, $total_users );\n\t\t\t$user_id     = (int) $wpdb->get_var( \"SELECT ID FROM {$wpdb->users} ORDER BY rand() LIMIT $offset, 1\" ); // phpcs:ignore\n\t\t\treturn new \\WC_Customer( $user_id );\n\t\t}\n\n\t\t$customer = Customer::generate( ! $guest );\n\n\t\tif ( ! $customer instanceof \\WC_Customer ) {\n\t\t\terror_log( 'Customer generation failed: Customer::generate() returned invalid result' );\n\t\t}\n\n\t\treturn $customer;\n\t}\n\n\t/**\n\t * Returns a date to use as the order date. If no date arguments have been passed, this will\n\t * return the current date. If a `date-start` argument is provided, a random date will be chosen\n\t * between `date-start` and the current date. You can pass an `end-date` and a random date between start\n\t * and end will be chosen.\n\t *\n\t * @param array $assoc_args CLI arguments.\n\t * @return string Date string (Y-m-d)\n\t */\n\tprotected static function get_date_created( $assoc_args ) {\n\t\t$current = date( 'Y-m-d', time() );\n\t\tif ( ! empty( $assoc_args['date-start'] ) && empty( $assoc_args['date-end'] ) ) {\n\t\t\t$start = $assoc_args['date-start'];\n\t\t\t$end   = $current;\n\t\t} elseif ( ! empty( $assoc_args['date-start'] ) && ! empty( $assoc_args['date-end'] ) ) {\n\t\t\t$start = $assoc_args['date-start'];\n\t\t\t$end   = $assoc_args['date-end'];\n\t\t} else {\n\t\t\treturn $current;\n\t\t}\n\n\t\t// Use timestamp-based random selection for single order generation\n\t\t$start_timestamp = strtotime( $start );\n\t\t$end_timestamp   = strtotime( $end );\n\t\t$days_between    = (int) ( ( $end_timestamp - $start_timestamp ) / DAY_IN_SECONDS );\n\n\t\t// If start and end are the same day, return that date (time will be randomized in generate())\n\t\tif ( 0 === $days_between ) {\n\t\t\treturn date( 'Y-m-d', $start_timestamp );\n\t\t}\n\n\t\t// Generate random offset in days and add to start timestamp\n\t\t$random_days = wp_rand( 0, $days_between );\n\t\treturn date( 'Y-m-d', $start_timestamp + ( $random_days * DAY_IN_SECONDS ) );\n\t}\n\n\t/**\n\t * Returns a status to use as the order's status. If no status argument has been passed, this will\n\t * return a random status.\n\t *\n\t * @param array $assoc_args CLI arguments.\n\t * @return string An order status.\n\t */\n\tprivate static function get_status( $assoc_args ) {\n\t\tif ( ! empty( $assoc_args['status'] ) ) {\n\t\t\treturn $assoc_args['status'];\n\t\t} else {\n\t\t\treturn self::random_weighted_element( array(\n\t\t\t\t'completed'  => 70,\n\t\t\t\t'processing' => 15,\n\t\t\t\t'on-hold'    => 5,\n\t\t\t\t'failed'     => 10,\n\t\t\t) );\n\t\t}\n\t}\n\n\t/**\n\t *  Get random products selected from existing products.\n\t *\n\t * @param int $min_amount Minimum amount of products to get.\n\t * @param int $max_amount Maximum amount of products to get.\n\t * @return array Random list of products.\n\t */\n\tprotected static function get_random_products( int $min_amount = 1, int $max_amount = 4 ) {\n\t\tglobal $wpdb;\n\n\t\t$products = array();\n\n\t\t$num_existing_products = (int) $wpdb->get_var(\n\t\t\t\"SELECT COUNT( DISTINCT ID )\n\t\t\tFROM {$wpdb->posts}\n\t\t\tWHERE 1=1\n\t\t\tAND post_type='product'\n\t\t\tAND post_status='publish'\"\n\t\t);\n\n\t\tif ( $num_existing_products === 0 ) {\n\t\t\terror_log( 'No published products found in database' );\n\t\t\treturn array();\n\t\t}\n\n\t\t$num_products_to_get = wp_rand( $min_amount, $max_amount );\n\n\t\tif ( $num_products_to_get > $num_existing_products ) {\n\t\t\t$num_products_to_get = $num_existing_products;\n\t\t}\n\n\t\t$query = new \\WC_Product_Query( array(\n\t\t\t'limit'   => $num_products_to_get,\n\t\t\t'return'  => 'ids',\n\t\t\t'orderby' => 'rand',\n\t\t) );\n\n\t\t$product_ids = $query->get_products();\n\t\tif ( empty( $product_ids ) ) {\n\t\t\terror_log( 'WC_Product_Query returned no product IDs' );\n\t\t\treturn array();\n\t\t}\n\n\t\tforeach ( $product_ids as $product_id ) {\n\t\t\t$product = wc_get_product( $product_id );\n\n\t\t\tif ( ! $product ) {\n\t\t\t\terror_log( \"Failed to retrieve product with ID: {$product_id}\" );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( $product->is_type( 'variable' ) ) {\n\t\t\t\t$available_variations = $product->get_available_variations();\n\t\t\t\tif ( empty( $available_variations ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t$index      = self::$faker->numberBetween( 0, count( $available_variations ) - 1 );\n\t\t\t\t$variation = new \\WC_Product_Variation( $available_variations[ $index ]['variation_id'] );\n\t\t\t\tif ( $variation && $variation->exists() ) {\n\t\t\t\t\t$products[] = $variation;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t$products[] = $product;\n\t\t\t}\n\t\t}\n\n\t\treturn $products;\n\t}\n\n\t/**\n\t * Get a random existing coupon or create coupons if none exist.\n\t * If no coupons exist, creates 6 coupons: 3 fixed value and 3 percentage.\n\t *\n\t * @return \\WC_Coupon|false Coupon object or false if none available.\n\t */\n\tprotected static function get_or_create_coupon() {\n\t\t// Try to get a random existing coupon\n\t\t$coupon = Coupon::get_random();\n\n\t\t// If no coupons exist, create 6 (3 fixed, 3 percentage)\n\t\tif ( false === $coupon ) {\n\t\t\tif ( class_exists( 'WP_CLI' ) ) {\n\t\t\t\t\\WP_CLI::log( 'No coupons found. Creating 6 coupons (3 fixed cart $5-$50, 3 percentage 5%-25%)...' );\n\t\t\t}\n\n\t\t\t// Create 3 fixed cart coupons ($5-$50)\n\t\t\t$fixed_result = Coupon::batch( 3, array( 'min' => 5, 'max' => 50, 'discount_type' => 'fixed_cart' ) );\n\n\t\t\t// Create 3 percentage coupons (5%-25%)\n\t\t\t$percent_result = Coupon::batch( 3, array( 'min' => 5, 'max' => 25, 'discount_type' => 'percent' ) );\n\n\t\t// If coupon creation failed, return false\n\t\tif ( is_wp_error( $fixed_result ) || is_wp_error( $percent_result ) ) {\n\t\t\t$error_message = 'Coupon creation failed: ';\n\t\t\tif ( is_wp_error( $fixed_result ) ) {\n\t\t\t\t$error_message .= 'Fixed coupons error: ' . $fixed_result->get_error_message() . ' ';\n\t\t\t}\n\t\t\tif ( is_wp_error( $percent_result ) ) {\n\t\t\t\t$error_message .= 'Percentage coupons error: ' . $percent_result->get_error_message();\n\t\t\t}\n\t\t\terror_log( $error_message );\n\t\t\treturn false;\n\t\t}\n\n\t\t\t// Now get a random coupon from the ones we just created\n\t\t\t$coupon = Coupon::get_random();\n\t\t}\n\n\t\treturn $coupon;\n\t}\n\n\t/**\n\t * Create a refund for an order (either full or partial).\n\t *\n\t * @param \\WC_Order      $order The order to refund.\n\t * @param bool           $force_partial Force partial refund only (legacy parameter).\n\t * @param \\WC_Order_Refund|null $previous_refund Previous refund to base date on (for second refunds).\n\t * @param bool|null      $force_full Explicitly force full refund (overrides random logic).\n\t * @return \\WC_Order_Refund|false Refund object on success, false on failure.\n\t */\n\tprotected static function create_refund( $order, $force_partial = false, $previous_refund = null, $force_full = null ) {\n\t\tif ( ! $order instanceof \\WC_Order ) {\n\t\t\terror_log( \"Error: Order is not an instance of \\WC_Order: \" . print_r( $order, true ) );\n\t\t\treturn false;\n\t\t}\n\n\t\t// Check if order already has refunds\n\t\t$existing_refunds = $order->get_refunds();\n\t\tif ( ! empty( $existing_refunds ) ) {\n\t\t\t$force_partial = true;\n\t\t\t$force_full = false; // Can't do full refund if already has refunds\n\t\t}\n\n\t\t// Calculate already refunded quantities\n\t\t$refunded_qty_by_item = self::calculate_refunded_quantities( $existing_refunds );\n\n\t\t// Determine refund type (full or partial)\n\t\tif ( null !== $force_full ) {\n\t\t\t// Explicit full/partial specified (batch mode with exact ratios)\n\t\t\t$is_full_refund = $force_full;\n\t\t} else {\n\t\t\t// Legacy random logic (single order generation or old code)\n\t\t\t$is_full_refund = $force_partial ? false : wp_rand( 0, 1 );\n\t\t}\n\n\t\t// Build refund line items\n\t\t$line_items = $is_full_refund\n\t\t\t? self::build_full_refund_items( $order, $refunded_qty_by_item )\n\t\t\t: self::build_partial_refund_items( $order, $refunded_qty_by_item );\n\n\t\t// Ensure we have items to refund\n\t\tif ( empty( $line_items ) ) {\n\t\t\terror_log( sprintf(\n\t\t\t\t'Refund skipped for order %d: No line items to refund. Order has %d items.',\n\t\t\t\t$order->get_id(),\n\t\t\t\tcount( $order->get_items( array( 'line_item', 'fee' ) ) )\n\t\t\t) );\n\t\t\treturn false;\n\t\t}\n\n\t\t// Calculate refund totals\n\t\t$totals = self::calculate_refund_totals( $line_items );\n\t\t$refund_amount = $totals['amount'];\n\t\t$total_items = $totals['total_items'];\n\t\t$total_qty = $totals['total_qty'];\n\n\t\t// For full refunds, use order's actual remaining total to avoid rounding discrepancies\n\t\tif ( $is_full_refund ) {\n\t\t\t$refund_amount = round( $order->get_total() - $order->get_total_refunded(), 2 );\n\t\t}\n\n\t\t// For partial refunds, ensure refund is < 50% of order total\n\t\tif ( ! $is_full_refund ) {\n\t\t\t$max_partial_refund = $order->get_total() * self::MAX_PARTIAL_REFUND_RATIO;\n\n\t\t\t// Remove items until refund is under threshold\n\t\t\twhile ( $refund_amount >= $max_partial_refund && count( $line_items ) > 1 ) {\n\t\t\t\tunset( $line_items[ array_rand( $line_items ) ] );\n\t\t\t\t$totals = self::calculate_refund_totals( $line_items );\n\t\t\t\t$refund_amount = $totals['amount'];\n\t\t\t\t$total_items = $totals['total_items'];\n\t\t\t\t$total_qty = $totals['total_qty'];\n\t\t\t}\n\t\t}\n\n\t\t// Cap refund amount to maximum available\n\t\t$max_refund = round( $order->get_total() - $order->get_total_refunded(), 2 );\n\t\tif ( $refund_amount > $max_refund ) {\n\t\t\t$refund_amount = $max_refund;\n\t\t}\n\n\t\t// Validate refund amount\n\t\tif ( $refund_amount <= 0 ) {\n\t\t\terror_log( sprintf(\n\t\t\t\t'Refund skipped for order %d: Invalid refund amount (%s). Order total: %s, Already refunded: %s',\n\t\t\t\t$order->get_id(),\n\t\t\t\t$refund_amount,\n\t\t\t\t$order->get_total(),\n\t\t\t\t$order->get_total_refunded()\n\t\t\t) );\n\t\t\treturn false;\n\t\t}\n\n\t\t// Create refund reason\n\t\t$reason = $is_full_refund\n\t\t\t? 'Full refund'\n\t\t\t: sprintf(\n\t\t\t\t'Partial refund - %d %s, %d %s',\n\t\t\t\t$total_items,\n\t\t\t\t$total_items === 1 ? 'product' : 'products',\n\t\t\t\t$total_qty,\n\t\t\t\t$total_qty === 1 ? 'item' : 'items'\n\t\t\t);\n\n\t\t// Calculate refund date\n\t\t$refund_date = self::calculate_refund_date( $order, $previous_refund );\n\n\t\t// Create the refund\n\t\t$refund = wc_create_refund(\n\t\t\tarray(\n\t\t\t\t'order_id'     => $order->get_id(),\n\t\t\t\t'amount'       => $refund_amount,\n\t\t\t\t'reason'       => $reason,\n\t\t\t\t'line_items'   => $line_items,\n\t\t\t\t'date_created' => $refund_date,\n\t\t\t)\n\t\t);\n\n\t\tif ( is_wp_error( $refund ) ) {\n\t\t\terror_log( sprintf(\n\t\t\t\t\"Refund creation failed for order %d:\\nError: %s\\nCalculated Amount: %s\\nOrder Total: %s\\nOrder Refunded Total: %s\\nReason: %s\\nLine Items: %s\",\n\t\t\t\t$order->get_id(),\n\t\t\t\t$refund->get_error_message(),\n\t\t\t\t$refund_amount,\n\t\t\t\t$order->get_total(),\n\t\t\t\t$order->get_total_refunded(),\n\t\t\t\t$reason,\n\t\t\t\tprint_r( $line_items, true )\n\t\t\t) );\n\t\t\treturn false;\n\t\t}\n\n\t\t// Update order status to refunded if it's a full refund\n\t\tif ( $is_full_refund ) {\n\t\t\t$order->set_status( 'refunded' );\n\t\t\t$order->save();\n\t\t}\n\n\t\treturn $refund;\n\t}\n\n\t/**\n\t * Calculate already refunded quantities per item from existing refunds.\n\t *\n\t * @param array $existing_refunds Array of existing refund objects.\n\t * @return array Associative array of item_id => refunded_quantity.\n\t */\n\tprotected static function calculate_refunded_quantities( $existing_refunds ) {\n\t\t$refunded_qty_by_item = array();\n\n\t\tforeach ( $existing_refunds as $existing_refund ) {\n\t\t\tforeach ( $existing_refund->get_items( array( 'line_item', 'fee' ) ) as $refund_item ) {\n\t\t\t\t$item_id = $refund_item->get_meta( '_refunded_item_id' );\n\t\t\t\tif ( ! $item_id ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif ( ! isset( $refunded_qty_by_item[ $item_id ] ) ) {\n\t\t\t\t\t$refunded_qty_by_item[ $item_id ] = 0;\n\t\t\t\t}\n\t\t\t\t$refunded_qty_by_item[ $item_id ] += abs( $refund_item->get_quantity() );\n\t\t\t}\n\t\t}\n\n\t\treturn $refunded_qty_by_item;\n\t}\n\n\t/**\n\t * Build a refund line item with proper tax and total calculations.\n\t *\n\t * @param \\WC_Order_Item $item Order item to refund.\n\t * @param int            $refund_qty Quantity to refund.\n\t * @param int            $original_qty Original quantity of the item.\n\t * @return array Refund line item data.\n\t */\n\tprotected static function build_refund_line_item( $item, $refund_qty, $original_qty ) {\n\t\t$taxes      = $item->get_taxes();\n\t\t$refund_tax = array();\n\n\t\t// Prorate tax based on refund quantity\n\t\tif ( ! empty( $taxes['total'] ) && $original_qty > 0 ) {\n\t\t\tforeach ( $taxes['total'] as $tax_id => $tax_amount ) {\n\t\t\t\t$tax_per_unit = $tax_amount / $original_qty;\n\t\t\t\t$refund_tax[ $tax_id ] = ( $tax_per_unit * $refund_qty ) * -1;\n\t\t\t}\n\t\t}\n\n\t\t// Prorate the refund total based on refund quantity\n\t\t$total_per_unit = $original_qty > 0 ? $item->get_total() / $original_qty : 0;\n\t\t$refund_total = $total_per_unit * $refund_qty;\n\n\t\treturn array(\n\t\t\t'qty'          => $refund_qty,\n\t\t\t'refund_total' => $refund_total * -1,\n\t\t\t'refund_tax'   => $refund_tax,\n\t\t);\n\t}\n\n\t/**\n\t * Build line items for a full refund.\n\t *\n\t * @param \\WC_Order $order Order to refund.\n\t * @param array     $refunded_qty_by_item Already refunded quantities.\n\t * @return array Refund line items.\n\t */\n\tprotected static function build_full_refund_items( $order, $refunded_qty_by_item ) {\n\t\t$line_items = array();\n\n\t\tforeach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) {\n\t\t\t$original_qty = $item->get_quantity();\n\t\t\t$refunded_qty = isset( $refunded_qty_by_item[ $item_id ] ) ? $refunded_qty_by_item[ $item_id ] : 0;\n\t\t\t$remaining_qty = $original_qty - $refunded_qty;\n\n\t\t\t// Skip if nothing left to refund or invalid quantity\n\t\t\tif ( $remaining_qty <= 0 || $original_qty <= 0 ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$line_items[ $item_id ] = self::build_refund_line_item( $item, $remaining_qty, $original_qty );\n\t\t}\n\n\t\treturn $line_items;\n\t}\n\n\t/**\n\t * Build line items for a partial refund.\n\t *\n\t * @param \\WC_Order $order Order to refund.\n\t * @param array     $refunded_qty_by_item Already refunded quantities.\n\t * @return array Refund line items.\n\t */\n\tprotected static function build_partial_refund_items( $order, $refunded_qty_by_item ) {\n\t\t$items = $order->get_items( array( 'line_item', 'fee' ) );\n\t\t$line_items = array();\n\n\t\t// Decide whether to refund full items or partial quantities\n\t\t$refund_full_items = (bool) wp_rand( 0, 1 );\n\n\t\tif ( $refund_full_items && count( $items ) > 2 ) {\n\t\t\t// Refund a random subset of items completely (requires at least 3 items)\n\t\t\t$items_array  = array_values( $items );\n\t\t\t$num_to_refund = wp_rand( 1, count( $items_array ) - 1 );\n\t\t\t$items_to_refund = array_rand( $items_array, $num_to_refund );\n\n\t\t\t// Ensure $items_to_refund is always an array for consistent iteration\n\t\t\tif ( ! is_array( $items_to_refund ) ) {\n\t\t\t\t$items_to_refund = array( $items_to_refund );\n\t\t\t}\n\n\t\t\tforeach ( $items_to_refund as $index ) {\n\t\t\t\t$item = $items_array[ $index ];\n\t\t\t\t$item_id = $item->get_id();\n\t\t\t\t$original_qty = $item->get_quantity();\n\t\t\t\t$refunded_qty = isset( $refunded_qty_by_item[ $item_id ] ) ? $refunded_qty_by_item[ $item_id ] : 0;\n\t\t\t\t$remaining_qty = $original_qty - $refunded_qty;\n\n\t\t\t\t// Skip if nothing left to refund or invalid quantity\n\t\t\t\tif ( $remaining_qty <= 0 || $original_qty <= 0 ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$line_items[ $item_id ] = self::build_refund_line_item( $item, $remaining_qty, $original_qty );\n\t\t\t}\n\t\t} else {\n\t\t\t// Refund partial quantities of items\n\t\t\tforeach ( $items as $item_id => $item ) {\n\t\t\t\t$original_qty = $item->get_quantity();\n\t\t\t\t$refunded_qty = isset( $refunded_qty_by_item[ $item_id ] ) ? $refunded_qty_by_item[ $item_id ] : 0;\n\t\t\t\t$remaining_qty = $original_qty - $refunded_qty;\n\n\t\t\t\t// Skip if nothing left to refund, if only 1 remaining, or invalid quantity\n\t\t\t\tif ( $remaining_qty <= 1 || $original_qty <= 0 ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Only refund line items with remaining quantity > 1\n\t\t\t\tif ( 'line_item' === $item->get_type() ) {\n\t\t\t\t\t$refund_qty = wp_rand( 1, $remaining_qty - 1 );\n\t\t\t\t\t$line_items[ $item_id ] = self::build_refund_line_item( $item, $refund_qty, $original_qty );\n\t\t\t\t\tbreak; // Only refund one item partially\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If no items were added, refund one complete remaining item\n\t\t\tif ( empty( $line_items ) && count( $items ) > 0 ) {\n\t\t\t\t$items_array = array_values( $items );\n\t\t\t\tshuffle( $items_array );\n\n\t\t\t\tforeach ( $items_array as $item ) {\n\t\t\t\t\t$item_id = $item->get_id();\n\t\t\t\t\t$original_qty = $item->get_quantity();\n\t\t\t\t\t$refunded_qty = isset( $refunded_qty_by_item[ $item_id ] ) ? $refunded_qty_by_item[ $item_id ] : 0;\n\t\t\t\t\t$remaining_qty = $original_qty - $refunded_qty;\n\n\t\t\t\t\t// Skip if nothing left to refund or invalid quantity\n\t\t\t\t\tif ( $remaining_qty <= 0 || $original_qty <= 0 ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t$line_items[ $item_id ] = self::build_refund_line_item( $item, $remaining_qty, $original_qty );\n\t\t\t\t\tbreak; // Only refund one item\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $line_items;\n\t}\n\n\t/**\n\t * Calculate total refund amount and item counts from line items.\n\t *\n\t * @param array $line_items Refund line items.\n\t * @return array Array containing 'amount', 'total_items', and 'total_qty'.\n\t */\n\tprotected static function calculate_refund_totals( $line_items ) {\n\t\t$refund_amount = 0;\n\t\t$total_items   = 0;\n\t\t$total_qty     = 0;\n\n\t\tforeach ( $line_items as $item_data ) {\n\t\t\t// Add item total: refund amounts are stored as negative, convert to positive for total calculation\n\t\t\t$refund_amount += abs( $item_data['refund_total'] );\n\t\t\t$total_items++;\n\t\t\t$total_qty += $item_data['qty'];\n\n\t\t\t// Add tax amounts\n\t\t\tif ( ! empty( $item_data['refund_tax'] ) ) {\n\t\t\t\tforeach ( $item_data['refund_tax'] as $tax_amount ) {\n\t\t\t\t\t$refund_amount += abs( $tax_amount );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\t'amount'      => round( $refund_amount, 2 ),\n\t\t\t'total_items' => $total_items,\n\t\t\t'total_qty'   => $total_qty,\n\t\t);\n\t}\n\n\t/**\n\t * Calculate a realistic refund date based on order completion or previous refund.\n\t * Ensures refund dates never exceed current time and second refunds always occur after first.\n\t *\n\t * @param \\WC_Order             $order Order being refunded.\n\t * @param \\WC_Order_Refund|null $previous_refund Previous refund (for second refunds).\n\t * @return string Refund date in 'Y-m-d H:i:s' format.\n\t */\n\tprotected static function calculate_refund_date( $order, $previous_refund = null ) {\n\t\t$now = time();\n\n\t\tif ( $previous_refund ) {\n\t\t\t// Second refund: must be after first refund but before current time\n\t\t\t$base_timestamp = strtotime( $previous_refund->get_date_created()->date( 'Y-m-d H:i:s' ) );\n\t\t\t$max_timestamp = min( $base_timestamp + ( self::SECOND_REFUND_MAX_DAYS * DAY_IN_SECONDS ), $now );\n\n\t\t\t// Ensure second refund is always after first refund\n\t\t\tif ( $max_timestamp <= $base_timestamp ) {\n\t\t\t\t// If there's no time window, use base timestamp + 1 hour\n\t\t\t\t// If base is in the future, second refund will also be in the future (but after first)\n\t\t\t\t$refund_timestamp = $base_timestamp + HOUR_IN_SECONDS;\n\t\t\t} else {\n\t\t\t\t$refund_timestamp = wp_rand( $base_timestamp + 1, $max_timestamp );\n\t\t\t}\n\t\t} else {\n\t\t\t// First refund: within 2 months of order completion, but never in the future\n\t\t\t$completion_timestamp = strtotime( $order->get_date_completed()->date( 'Y-m-d H:i:s' ) );\n\t\t\t$max_timestamp = min( $completion_timestamp + ( self::FIRST_REFUND_MAX_DAYS * DAY_IN_SECONDS ), $now );\n\n\t\t\t// Ensure we have a valid time window\n\t\t\tif ( $max_timestamp < $completion_timestamp ) {\n\t\t\t\t// Order completed in the future somehow, use current time\n\t\t\t\t$refund_timestamp = $now;\n\t\t\t} elseif ( $max_timestamp == $completion_timestamp ) {\n\t\t\t\t// No time window, use completion timestamp\n\t\t\t\t$refund_timestamp = $completion_timestamp;\n\t\t\t} else {\n\t\t\t\t$refund_timestamp = wp_rand( $completion_timestamp, $max_timestamp );\n\t\t\t}\n\t\t}\n\n\t\treturn date( 'Y-m-d H:i:s', $refund_timestamp );\n\t}\n\n\t/**\n\t * Generate an array of sorted dates for batch order creation.\n\t * Ensures chronological order when creating multiple orders.\n\t *\n\t * @param int   $count Number of dates to generate.\n\t * @param array $args  Arguments containing date-start and optional date-end.\n\t * @return array Sorted array of date strings (Y-m-d).\n\t */\n\tprotected static function generate_batch_dates( $count, $args ) {\n\t\t$current = date( 'Y-m-d', time() );\n\n\t\tif ( ! empty( $args['date-start'] ) && empty( $args['date-end'] ) ) {\n\t\t\t$start = $args['date-start'];\n\t\t\t$end   = $current;\n\t\t} elseif ( ! empty( $args['date-start'] ) && ! empty( $args['date-end'] ) ) {\n\t\t\t$start = $args['date-start'];\n\t\t\t$end   = $args['date-end'];\n\t\t} else {\n\t\t\t// No date range specified, return array of current dates\n\t\t\treturn array_fill( 0, $count, $current );\n\t\t}\n\n\t\t$start_timestamp = strtotime( $start );\n\t\t$end_timestamp   = strtotime( $end );\n\t\t$days_between    = (int) ( ( $end_timestamp - $start_timestamp ) / DAY_IN_SECONDS );\n\n\t\t// If start and end dates are the same, return array of that date\n\t\tif ( 0 === $days_between ) {\n\t\t\treturn array_fill( 0, $count, date( 'Y-m-d', $start_timestamp ) );\n\t\t}\n\n\t\t$dates = array();\n\t\tfor ( $i = 0; $i < $count; $i++ ) {\n\t\t\t$random_days = wp_rand( 0, $days_between );\n\t\t\t$dates[] = date( 'Y-m-d', $start_timestamp + ( $random_days * DAY_IN_SECONDS ) );\n\t\t}\n\n\t\t// Sort chronologically so lower order IDs get earlier dates\n\t\tsort( $dates );\n\n\t\treturn $dates;\n\t}\n\n}\n"
  },
  {
    "path": "includes/Generator/OrderAttribution.php",
    "content": "<?php\n/**\n * Order Attribution data helper.\n *\n * @package SmoothGenerator\\Classes\n */\n\nnamespace WC\\SmoothGenerator\\Generator;\n\n/**\n * Order Attribution data helper class.\n */\nclass OrderAttribution {\n\n\t/**\n\t * Campaign distribution percentages\n\t */\n\tconst CAMPAIGN_PROBABILITY = 15; // 15% of orders will have campaign data\n\n\t/**\n\t * Generate order attribution data.\n\t *\n\t * @param \\WC_Order $order Order.\n\t * @param array     $assoc_args Arguments passed via the CLI for additional customization.\n\t */\n\tpublic static function add_order_attribution_meta( $order, $assoc_args = array() ) {\n\n\t\tif ( isset( $assoc_args['skip-order-attribution'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$order_products = $order->get_items();\n\n\t\t$device_type = self::get_random_device_type();\n\t\t$source      = 'woocommerce.com';\n\t\t$source_type = self::get_source_type();\n\t\t$origin      = self::get_origin( $source_type, $source );\n\t\t$product_url = empty( $order_products ) ? '' : get_permalink( $order_products[ array_rand( $order_products ) ]->get_id() );\n\t\t$utm_content = array( '/', 'campaign_a', 'campaign_b' );\n\t\t$utm_content = $utm_content[ array_rand( $utm_content ) ];\n\n\t\t$meta = array();\n\n\t\t// For these source types, we only need to set the source type.\n\t\tif ( in_array( $source_type, array( 'admin', 'mobile_app', 'unknown' ), true ) ) {\n\t\t\t$meta = array(\n\t\t\t\t'_wc_order_attribution_source_type' => $source_type,\n\t\t\t);\n\t\t} else {\n\t\t\t$meta = array(\n\t\t\t\t'_wc_order_attribution_origin'             => $origin,\n\t\t\t\t'_wc_order_attribution_device_type'        => $device_type,\n\t\t\t\t'_wc_order_attribution_user_agent'         => self::get_random_user_agent_for_device( $device_type ),\n\t\t\t\t'_wc_order_attribution_session_count'      => wp_rand( 1, 10 ),\n\t\t\t\t'_wc_order_attribution_session_pages'      => wp_rand( 1, 10 ),\n\t\t\t\t'_wc_order_attribution_session_start_time' => self::get_random_session_start_time( $order ),\n\t\t\t\t'_wc_order_attribution_session_entry'      => $product_url,\n\t\t\t\t'_wc_order_attribution_utm_content'        => $utm_content,\n\t\t\t\t'_wc_order_attribution_utm_source'         => self::get_source( $source_type ),\n\t\t\t\t'_wc_order_attribution_referrer'           => self::get_referrer( $source_type ),\n\t\t\t\t'_wc_order_attribution_source_type'        => $source_type,\n\t\t\t);\n\n\t\t\t// Add campaign data only for a percentage of orders.\n\t\t\tif ( wp_rand( 1, 100 ) <= self::CAMPAIGN_PROBABILITY ) {\n\t\t\t\t$campaign_data = self::get_campaign_data();\n\t\t\t\t$meta          = array_merge( $meta, $campaign_data );\n\t\t\t}\n\t\t}\n\n\t\t// If the source type is not typein ( Direct ), set a random utm medium.\n\t\tif ( ! in_array( $source_type, array( 'typein', 'admin', 'mobile_app', 'unknown' ), true ) ) {\n\t\t\t$meta['_wc_order_attribution_utm_medium'] = self::get_random_utm_medium();\n\t\t}\n\n\t\tforeach ( $meta as $key => $value ) {\n\t\t\t$order->add_meta_data( $key, $value );\n\t\t}\n\t}\n\n\t/**\n\t * Get a random referrer based on the source type.\n\t *\n\t * @param string $source_type The source type.\n\t * @return string The referrer.\n\t */\n\tpublic static function get_referrer( string $source_type ) {\n\t\t// Set up the label based on the source type.\n\t\tswitch ( $source_type ) {\n\t\t\tcase 'utm':\n\t\t\t\t$utm = array(\n\t\t\t\t\t'https://woocommerce.com/',\n\t\t\t\t\t'https://twitter.com',\n\t\t\t\t);\n\t\t\t\treturn $utm[ array_rand( $utm ) ];\n\t\t\tcase 'organic':\n\t\t\t\t$organic = array(\n\t\t\t\t\t'https://google.com',\n\t\t\t\t\t'https://bing.com',\n\t\t\t\t);\n\t\t\t\treturn $organic[ array_rand( $organic ) ];\n\t\t\tcase 'referral':\n\t\t\t\t$refferal = array(\n\t\t\t\t\t'https://woocommerce.com/',\n\t\t\t\t\t'https://facebook.com',\n\t\t\t\t\t'https://twitter.com',\n\t\t\t\t\t'https://chatgpt.com',\n\t\t\t\t\t'https://claude.ai',\n\t\t\t\t);\n\t\t\t\treturn $refferal[ array_rand( $refferal ) ];\n\t\t\tcase 'typein':\n\t\t\t\treturn '';\n\t\t\tcase 'admin':\n\t\t\t\treturn '';\n\t\t\tcase 'mobile_app':\n\t\t\t\treturn '';\n\t\t\tdefault:\n\t\t\t\treturn '';\n\t\t}\n\t}\n\n\t/**\n\t * Get a random utm medium.\n\t *\n\t * @return string The utm medium.\n\t */\n\tpublic static function get_random_utm_medium() {\n\t\t$utm_mediums = array(\n\t\t\t'referral',\n\t\t\t'cpc',\n\t\t\t'email',\n\t\t\t'social',\n\t\t\t'organic',\n\t\t\t'unknown',\n\t\t);\n\n\t\treturn $utm_mediums[ array_rand( $utm_mediums ) ];\n\t}\n\n\t/**\n\t * Get the origin.\n\t *\n\t * @param string $source_type The source type.\n\t * @param string $source The source.\n\t *\n\t * @return string The origin.\n\t */\n\tpublic static function get_origin( string $source_type, string $source ) {\n\t\t// Set up the label based on the source type.\n\t\tswitch ( $source_type ) {\n\t\t\tcase 'utm':\n\t\t\t\treturn 'Source: ' . $source;\n\t\t\tcase 'organic':\n\t\t\t\treturn 'Organic: ' . $source;\n\t\t\tcase 'referral':\n\t\t\t\treturn 'Referral: ' . $source;\n\t\t\tcase 'typein':\n\t\t\t\treturn 'Direct';\n\t\t\tcase 'admin':\n\t\t\t\treturn 'Web admin';\n\t\t\tcase 'mobile_app':\n\t\t\t\treturn 'Mobile app';\n\t\t\tdefault:\n\t\t\t\treturn 'Unknown';\n\t\t}\n\t}\n\n\t/**\n\t * Get random source type.\n\t *\n\t * @return string The source type.\n\t */\n\tpublic static function get_source_type() {\n\t\t$source_types = array(\n\t\t\t'typein',\n\t\t\t'organic',\n\t\t\t'referral',\n\t\t\t'utm',\n\t\t\t'admin',\n\t\t\t'mobile_app',\n\t\t\t'unknown',\n\t\t);\n\n\t\treturn $source_types[ array_rand( $source_types ) ];\n\t}\n\n\t/**\n\t * Get random source based on the source type.\n\t *\n\t * @param string $source_type The source type.\n\t * @return string The source.\n\t */\n\tpublic static function get_source( $source_type ) {\n\t\tswitch ( $source_type ) {\n\t\t\tcase 'typein':\n\t\t\t\treturn '(direct)';\n\t\t\tcase 'organic':\n\t\t\t\t$organic = array(\n\t\t\t\t\t'google',\n\t\t\t\t\t'bing',\n\t\t\t\t\t'yahoo',\n\t\t\t\t);\n\t\t\t\treturn $organic[ array_rand( $organic ) ];\n\t\t\tcase 'referral':\n\t\t\t\t$refferal = array(\n\t\t\t\t\t'woocommerce.com',\n\t\t\t\t\t'facebook.com',\n\t\t\t\t\t'twitter.com',\n\t\t\t\t\t'chatgpt.com',\n\t\t\t\t\t'claude.ai',\n\t\t\t\t);\n\t\t\t\treturn $refferal[ array_rand( $refferal ) ];\n\t\t\tcase 'social':\n\t\t\t\t$social = array(\n\t\t\t\t\t'facebook.com',\n\t\t\t\t\t'twitter.com',\n\t\t\t\t\t'instagram.com',\n\t\t\t\t\t'pinterest.com',\n\t\t\t\t);\n\t\t\t\treturn $social[ array_rand( $social ) ];\n\t\t\tcase 'utm':\n\t\t\t\t$utm = array(\n\t\t\t\t\t'mailchimp',\n\t\t\t\t\t'google',\n\t\t\t\t\t'newsletter',\n\t\t\t\t);\n\t\t\t\treturn $utm[ array_rand( $utm ) ];\n\t\t\tdefault:\n\t\t\t\treturn 'Unknown';\n\t\t}\n\t}\n\n\t/**\n\t * Get random device type based on the following distribution:\n\t * Mobile:  50%\n\t * Desktop: 35%\n\t * Tablet:  15%\n\t */\n\tpublic static function get_random_device_type() {\n\t\t$randomNumber = wp_rand( 1, 100 ); // Generate a random number between 1 and 100.\n\n\t\tif ( $randomNumber <= 50 ) {\n\t\t\treturn 'Mobile';\n\t\t} elseif ( $randomNumber <= 85 ) {\n\t\t\treturn 'Desktop';\n\t\t} else {\n\t\t\treturn 'Tablet';\n\t\t}\n\t}\n\n\t/**\n\t * Get a random user agent based on the device type.\n\t *\n\t * @param string $device_type The device type.\n\t * @return string The user agent.\n\t */\n\tpublic static function get_random_user_agent_for_device( $device_type ) {\n\t\tswitch ( $device_type ) {\n\t\t\tcase 'Mobile':\n\t\t\t\treturn self::get_random_mobile_user_agent();\n\t\t\tcase 'Tablet':\n\t\t\t\treturn self::get_random_tablet_user_agent();\n\t\t\tcase 'Desktop':\n\t\t\t\treturn self::get_random_desktop_user_agent();\n\t\t\tdefault:\n\t\t\t\treturn '';\n\t\t}\n\t}\n\n\t/**\n\t * Get a random mobile user agent.\n\t *\n\t * @return string The user agent.\n\t */\n\tpublic static function get_random_mobile_user_agent() {\n\t\t$user_agents = array(\n\t\t\t'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1',\n\t\t\t'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/114.0.5735.99 Mobile/15E148 Safari/604.1',\n\t\t\t'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36',\n\t\t\t'Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/21.0 Chrome/110.0.5481.154 Mobile Safari/537.36',\n\t\t);\n\n\t\treturn $user_agents[ array_rand( $user_agents ) ];\n\t}\n\n\t/**\n\t * Get a random tablet user agent.\n\t *\n\t * @return string The user agent.\n\t */\n\tpublic static function get_random_tablet_user_agent() {\n\t\t$user_agents = array(\n\t\t\t'Mozilla/5.0 (iPad; CPU OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/114.0.5735.124 Mobile/15E148 Safari/604.1',\n\t\t\t'Mozilla/5.0 (Linux; Android 12; SM-X906C Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36',\n\t\t);\n\n\t\treturn $user_agents[ array_rand( $user_agents ) ];\n\t}\n\n\t/**\n\t * Get a random desktop user agent.\n\t *\n\t * @return string The user agent.\n\t */\n\tpublic static function get_random_desktop_user_agent() {\n\t\t$user_agents = array(\n\t\t\t'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246',\n\t\t\t'Mozilla/5.0 (X11; CrOS x86_64 8172.45.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.64 Safari/537.36',\n\t\t\t'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9',\n\t\t);\n\n\t\treturn $user_agents[ array_rand( $user_agents ) ];\n\t}\n\n\t/**\n\t * Get a random session start time based on the order creation time.\n\t *\n\t * @param \\WC_Order $order The order.\n\t * @return string The session start time.\n\t */\n\tpublic static function get_random_session_start_time( $order ) {\n\n\t\t// Clone the order creation date so we don't modify the original.\n\t\t$order_created_date = clone $order->get_date_created();\n\n\t\t// Random DateTimeInterval between 10 minutes and 6 hours.\n\t\t$random_interval = new \\DateInterval( 'PT' . (string) wp_rand( 10, 360 ) . 'M' );\n\n\t\t// Subtract the random interval from the order creation date.\n\t\t$order_created_date->sub( $random_interval );\n\n\t\treturn $order_created_date->format( 'Y-m-d H:i:s' );\n\t}\n\n\t/**\n\t * Get campaign attribution data.\n\t *\n\t * @return array Campaign attribution data.\n\t */\n\tprivate static function get_campaign_data() {\n\t\t$campaign_type = self::get_campaign_type();\n\n\t\tswitch ( $campaign_type ) {\n\t\t\tcase 'seasonal':\n\t\t\t\treturn self::get_seasonal_campaign_data();\n\t\t\tcase 'promotional':\n\t\t\t\treturn self::get_promotional_campaign_data();\n\t\t\tcase 'product':\n\t\t\t\treturn self::get_product_campaign_data();\n\t\t\tdefault:\n\t\t\t\treturn self::get_general_campaign_data();\n\t\t}\n\t}\n\n\t/**\n\t * Get the campaign type based on weighted probabilities.\n\t *\n\t * @return string Campaign type.\n\t */\n\tprivate static function get_campaign_type() {\n\t\t$random = wp_rand( 1, 100 );\n\n\t\tif ( $random <= 40 ) {\n\t\t\treturn 'seasonal'; // 40% seasonal campaigns\n\t\t} elseif ( $random <= 70 ) {\n\t\t\treturn 'promotional'; // 30% promotional campaigns\n\t\t} elseif ( $random <= 90 ) {\n\t\t\treturn 'product'; // 20% product campaigns\n\t\t} else {\n\t\t\treturn 'general'; // 10% general campaigns\n\t\t}\n\t}\n\n\t/**\n\t * Get seasonal campaign data.\n\t *\n\t * @return array Campaign data.\n\t */\n\tprivate static function get_seasonal_campaign_data() {\n\t\t$campaigns = array(\n\t\t\t'summer_sale'  => array(\n\t\t\t\t'content' => 'summer_deals',\n\t\t\t\t'term'    => 'seasonal_discount',\n\t\t\t),\n\t\t\t'black_friday' => array(\n\t\t\t\t'content' => 'bf_deals',\n\t\t\t\t'term'    => 'black_friday_sale',\n\t\t\t),\n\t\t\t'holiday_special'   => array(\n\t\t\t\t'content' => 'holiday_deals',\n\t\t\t\t'term'    => 'christmas_sale',\n\t\t\t),\n\t\t);\n\n\t\t$campaign_name = array_rand( $campaigns );\n\t\t$campaign      = $campaigns[ $campaign_name ];\n\n\t\treturn array(\n\t\t\t'_wc_order_attribution_utm_campaign' => $campaign_name,\n\t\t\t'_wc_order_attribution_utm_content'  => $campaign['content'],\n\t\t\t'_wc_order_attribution_utm_term'     => $campaign['term'],\n\t\t);\n\t}\n\n\t/**\n\t * Get promotional campaign data.\n\t *\n\t * @return array Campaign data.\n\t */\n\tprivate static function get_promotional_campaign_data() {\n\t\t$campaigns = array(\n\t\t\t'flash_sale'       => array(\n\t\t\t\t'content' => '24hr_sale',\n\t\t\t\t'term'    => 'limited_time',\n\t\t\t),\n\t\t\t'membership_promo' => array(\n\t\t\t\t'content' => 'member_exclusive',\n\t\t\t\t'term'    => 'join_now',\n\t\t\t),\n\t\t);\n\n\t\t$campaign_name = array_rand( $campaigns );\n\t\t$campaign      = $campaigns[ $campaign_name ];\n\n\t\treturn array(\n\t\t\t'_wc_order_attribution_utm_campaign' => $campaign_name,\n\t\t\t'_wc_order_attribution_utm_content'  => $campaign['content'],\n\t\t\t'_wc_order_attribution_utm_term'     => $campaign['term'],\n\t\t);\n\t}\n\n\t/**\n\t * Get product campaign data.\n\t *\n\t * @return array Campaign data.\n\t */\n\tprivate static function get_product_campaign_data() {\n\t\t$campaigns = array(\n\t\t\t'new_product_launch' => array(\n\t\t\t\t'content' => 'product_launch',\n\t\t\t\t'term'    => 'new_arrival',\n\t\t\t),\n\t\t\t'spring_collection'  => array(\n\t\t\t\t'content' => 'spring',\n\t\t\t\t'term'    => 'new_collection',\n\t\t\t),\n\t\t);\n\n\t\t$campaign_name = array_rand( $campaigns );\n\t\t$campaign      = $campaigns[ $campaign_name ];\n\n\t\treturn array(\n\t\t\t'_wc_order_attribution_utm_campaign' => $campaign_name,\n\t\t\t'_wc_order_attribution_utm_content'  => $campaign['content'],\n\t\t\t'_wc_order_attribution_utm_term'     => $campaign['term'],\n\t\t);\n\t}\n\n\t/**\n\t * Get general campaign data.\n\t *\n\t * @return array Campaign data.\n\t */\n\tprivate static function get_general_campaign_data() {\n\t\t$campaigns = array(\n\t\t\t'newsletter_special' => array(\n\t\t\t\t'content' => 'newsletter_special',\n\t\t\t\t'term'    => 'newsletter_special',\n\t\t\t),\n\t\t\t'social_campaign'    => array(\n\t\t\t\t'content' => 'social_campaign',\n\t\t\t\t'term'    => 'social_campaign',\n\t\t\t),\n\t\t\t'influencer_collab'  => array(\n\t\t\t\t'content' => 'influencer_collab',\n\t\t\t\t'term'    => 'influencer_collab',\n\t\t\t),\n\t\t);\n\n\t\t$campaign_name = array_rand( $campaigns );\n\t\t$campaign      = $campaigns[ $campaign_name ];\n\n\t\treturn array(\n\t\t\t'_wc_order_attribution_utm_campaign' => $campaign_name,\n\t\t\t'_wc_order_attribution_utm_content'  => $campaign['content'],\n\t\t\t'_wc_order_attribution_utm_term'     => $campaign['term'],\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "includes/Generator/Product.php",
    "content": "<?php\n/**\n * Abstract product generator class\n *\n * @package SmoothGenerator\\Abstracts\n */\n\nnamespace WC\\SmoothGenerator\\Generator;\n\nuse WC\\SmoothGenerator\\Util\\RandomRuntimeCache;\n\n/**\n * Product data generator.\n */\nclass Product extends Generator {\n\t/**\n\t * Holds array of product IDs for generating relationships.\n\t *\n\t * @var array Array of IDs.\n\t */\n\tprotected static $product_ids = array();\n\n\t/**\n\t * Holds array of global attributes new products may reuse.\n\t *\n\t * @var array Array of attributes.\n\t */\n\tprotected static $global_attributes = array(\n\t\t'Color'        => array(\n\t\t\t'Green',\n\t\t\t'Blue',\n\t\t\t'Red',\n\t\t\t'Yellow',\n\t\t\t'Indigo',\n\t\t\t'Violet',\n\t\t\t'Black',\n\t\t\t'White',\n\t\t\t'Orange',\n\t\t\t'Pink',\n\t\t\t'Purple',\n\t\t),\n\t\t'Size'         => array(\n\t\t\t'Small',\n\t\t\t'Medium',\n\t\t\t'Large',\n\t\t\t'XL',\n\t\t\t'XXL',\n\t\t\t'XXXL',\n\t\t),\n\t\t'Numeric Size' => array(\n\t\t\t'6',\n\t\t\t'7',\n\t\t\t'8',\n\t\t\t'9',\n\t\t\t'10',\n\t\t\t'11',\n\t\t\t'12',\n\t\t\t'13',\n\t\t\t'14',\n\t\t\t'15',\n\t\t\t'16',\n\t\t\t'17',\n\t\t\t'18',\n\t\t\t'19',\n\t\t\t'20',\n\t\t),\n\t);\n\n\t/**\n\t * Return a new product.\n\t *\n\t * @param bool  $save Save the object before returning or not.\n\t * @param array $assoc_args Arguments passed via the CLI for additional customization.\n\t * @return \\WC_Product|\\WP_Error The product object consisting of random data, or WP_Error on failure.\n\t */\n\tpublic static function generate( $save = true, $assoc_args = array() ) {\n\t\tparent::maybe_initialize_generators();\n\n\t\t$type = self::get_product_type( $assoc_args );\n\t\tswitch ( $type ) {\n\t\t\tcase 'simple':\n\t\t\tdefault:\n\t\t\t\t$product = self::generate_simple_product();\n\t\t\t\tbreak;\n\t\t\tcase 'variable':\n\t\t\t\t$product = self::generate_variable_product();\n\t\t\t\tbreak;\n\t\t}\n\n\t\t// Check if product generation failed.\n\t\tif ( is_wp_error( $product ) ) {\n\t\t\treturn $product;\n\t\t}\n\n\t\tif ( $product ) {\n\t\t\t$product->save();\n\n\t\t\t// Assign brand terms using wp_set_object_terms, but only if the taxonomy exists.\n\t\t\tif ( taxonomy_exists( 'product_brand' ) ) {\n\t\t\t\t$brand_ids = self::get_term_ids( 'product_brand', self::$faker->numberBetween( 1, 3 ) );\n\t\t\t\tif ( ! empty( $brand_ids ) ) {\n\t\t\t\t\t$brand_result = wp_set_object_terms( $product->get_id(), $brand_ids, 'product_brand' );\n\t\t\t\t\tif ( is_wp_error( $brand_result ) ) {\n\t\t\t\t\t\treturn $brand_result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Limit size of stored relationship IDs.\n\t\tif ( 100 < count( self::$product_ids ) ) {\n\t\t\tshuffle( self::$product_ids );\n\t\t\tself::$product_ids = array_slice( self::$product_ids, 0, 50 );\n\t\t}\n\n\t\tself::$product_ids[] = $product->get_id();\n\n\t\t/**\n\t\t * Action: Product generator returned a new product.\n\t\t *\n\t\t * @since 1.2.0\n\t\t *\n\t\t * @param \\WC_Product $product\n\t\t */\n\t\tdo_action( 'smoothgenerator_product_generated', $product );\n\n\t\treturn $product;\n\t}\n\n\t/**\n\t * Create multiple products.\n\t *\n\t * @param int   $amount   The number of products to create.\n\t * @param array $args     Additional args for product creation.\n\t *\n\t * @return int[]|\\WP_Error\n\t */\n\tpublic static function batch( $amount, array $args = array() ) {\n\t\t$amount = self::validate_batch_amount( $amount );\n\t\tif ( is_wp_error( $amount ) ) {\n\t\t\treturn $amount;\n\t\t}\n\n\t\t$use_existing_terms = ! empty( $args['use-existing-terms'] );\n\t\tif ( ! $use_existing_terms ) {\n\t\t\tself::maybe_generate_terms( $amount );\n\t\t}\n\n\t\t$product_ids = array();\n\n\t\tfor ( $i = 1; $i <= $amount; $i++ ) {\n\t\t\t$product = self::generate( true, $args );\n\n\t\t\t// Skip products that failed to generate.\n\t\t\tif ( is_wp_error( $product ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$product_ids[] = $product->get_id();\n\t\t}\n\n\t\t// In case multiple batches are being run in one request, refresh the cache data.\n\t\tRandomRuntimeCache::clear( 'product_cat' );\n\t\tRandomRuntimeCache::clear( 'product_tag' );\n\t\tRandomRuntimeCache::clear( 'product_brand' );\n\n\t\treturn $product_ids;\n\t}\n\n\t/**\n\t * Create a new global attribute.\n\t *\n\t * @param string $raw_name Attribute name (label).\n\t * @return int Attribute ID.\n\t */\n\tprotected static function create_global_attribute( $raw_name ) {\n\t\t$slug = wc_sanitize_taxonomy_name( $raw_name );\n\n\t\t$attribute_id = wc_create_attribute( array(\n\t\t\t'name'         => $raw_name,\n\t\t\t'slug'         => $slug,\n\t\t\t'type'         => 'select',\n\t\t\t'order_by'     => 'menu_order',\n\t\t\t'has_archives' => false,\n\t\t) );\n\n\t\t$taxonomy_name = wc_attribute_taxonomy_name( $slug );\n\t\tregister_taxonomy(\n\t\t\t$taxonomy_name,\n\t\t\tapply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ),\n\t\t\tapply_filters( 'woocommerce_taxonomy_args_' . $taxonomy_name, array(\n\t\t\t\t'labels'       => array(\n\t\t\t\t\t'name' => $raw_name,\n\t\t\t\t),\n\t\t\t\t'hierarchical' => true,\n\t\t\t\t'show_ui'      => false,\n\t\t\t\t'query_var'    => true,\n\t\t\t\t'rewrite'      => false,\n\t\t\t) )\n\t\t);\n\n\t\tself::$global_attributes[ $raw_name ] = isset( self::$global_attributes[ $raw_name ] ) ? self::$global_attributes[ $raw_name ] : array();\n\n\t\tdelete_transient( 'wc_attribute_taxonomies' );\n\n\t\treturn $attribute_id;\n\t}\n\n\t/**\n\t * Generate attributes for a product.\n\t *\n\t * @param integer $qty Number of attributes to generate.\n\t * @param integer $maximum_terms Maximum number of terms per attribute to generate.\n\t * @return array|\\WP_Error Array of attributes or WP_Error on failure.\n\t */\n\tprotected static function generate_attributes( $qty = 1, $maximum_terms = 10 ) {\n\t\t$used_names = array();\n\t\t$attributes = array();\n\n\t\tfor ( $i = 0; $i < $qty; $i++ ) {\n\t\t\t$attribute = new \\WC_Product_Attribute();\n\t\t\t$attribute->set_id( 0 );\n\t\t\t$attribute->set_position( $i );\n\t\t\t$attribute->set_visible( true );\n\t\t\t$attribute->set_variation( true );\n\n\t\t\tif ( self::$faker->boolean() ) {\n\t\t\t\t$raw_name = array_rand( self::$global_attributes );\n\n\t\t\t\tif ( in_array( $raw_name, $used_names, true ) ) {\n\t\t\t\t\t$raw_name = ucfirst( substr( self::$faker->word(), 0, 28 ) );\n\t\t\t\t}\n\n\t\t\t\t$attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' );\n\t\t\t\t$attribute_name   = array_search( $raw_name, $attribute_labels, true );\n\n\t\t\t\tif ( ! $attribute_name ) {\n\t\t\t\t\t$attribute_name = wc_sanitize_taxonomy_name( $raw_name );\n\t\t\t\t}\n\n\t\t\t\t$attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name );\n\n\t\t\t\tif ( ! $attribute_id ) {\n\t\t\t\t\t$attribute_id = self::create_global_attribute( $raw_name );\n\n\t\t\t\t\t// Check if attribute creation failed.\n\t\t\t\t\tif ( is_wp_error( $attribute_id ) ) {\n\t\t\t\t\t\treturn $attribute_id;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t$slug          = wc_sanitize_taxonomy_name( $raw_name );\n\t\t\t\t$taxonomy_name = wc_attribute_taxonomy_name( $slug );\n\n\t\t\t\t$attribute->set_name( $taxonomy_name );\n\t\t\t\t$attribute->set_id( $attribute_id );\n\n\t\t\t\t$used_names[] = $raw_name;\n\n\t\t\t\t$num_values      = self::$faker->numberBetween( 1, $maximum_terms );\n\t\t\t\t$values          = array();\n\t\t\t\t$existing_values = isset( self::$global_attributes[ $raw_name ] ) ? self::$global_attributes[ $raw_name ] : array();\n\n\t\t\t\tfor ( $j = 0; $j < $num_values; $j++ ) {\n\t\t\t\t\t$value = '';\n\n\t\t\t\t\tif ( self::$faker->boolean( 80 ) && ! empty( $existing_values ) ) {\n\t\t\t\t\t\tshuffle( $existing_values );\n\t\t\t\t\t\t$value = array_pop( $existing_values );\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( empty( $value ) || in_array( $value, $values, true ) ) {\n\t\t\t\t\t\t$value = ucfirst( self::$faker->words( self::$faker->numberBetween( 1, 2 ), true ) );\n\t\t\t\t\t}\n\n\t\t\t\t\tself::$global_attributes[ $raw_name ][] = $value;\n\n\t\t\t\t\t$values[] = $value;\n\t\t\t\t}\n\t\t\t\t$attribute->set_options( $values );\n\t\t\t} else {\n\t\t\t\t$attribute->set_name( ucfirst( self::$faker->words( self::$faker->numberBetween( 1, 3 ), true ) ) );\n\t\t\t\t$attribute->set_options( array_filter( self::$faker->words( self::$faker->numberBetween( 2, 4 ), false ) ), 'ucfirst' );\n\t\t\t}\n\t\t\t$attributes[] = $attribute;\n\t\t}\n\n\t\treturn $attributes;\n\t}\n\n\t/**\n\t * Returns a product type to generate. If no type is specified, or an invalid type is specified,\n\t * a weighted random type is returned.\n\t *\n\t * @param array $assoc_args CLI arguments.\n\t * @return string A product type.\n\t */\n\tprotected static function get_product_type( array $assoc_args ) {\n\t\t$type  = $assoc_args['type'] ?? null;\n\t\t$types = array(\n\t\t\t'simple',\n\t\t\t'variable',\n\t\t);\n\n\t\tif ( ! is_null( $type ) && in_array( $type, $types, true ) ) {\n\t\t\treturn $type;\n\t\t} else {\n\t\t\treturn self::random_weighted_element( array(\n\t\t\t\t'simple'   => 80,\n\t\t\t\t'variable' => 20,\n\t\t\t) );\n\t\t}\n\t}\n\n\t/**\n\t * Generate a variable product and return it.\n\t *\n\t * @return \\WC_Product_Variable|\\WP_Error Product object or WP_Error on failure.\n\t */\n\tprotected static function generate_variable_product() {\n\t\t$name              = ucwords( self::$faker->productName );\n\t\t$will_manage_stock = self::$faker->boolean();\n\t\t$product           = new \\WC_Product_Variable();\n\n\t\t$gallery    = self::maybe_get_gallery_image_ids();\n\t\t$attributes = self::generate_attributes( self::$faker->numberBetween( 1, 3 ), 5 );\n\n\t\t// Check if attribute generation failed.\n\t\tif ( is_wp_error( $attributes ) ) {\n\t\t\treturn $attributes;\n\t\t}\n\n\t\t$product->set_props( array(\n\t\t\t'name'              => $name,\n\t\t\t'featured'          => self::$faker->boolean( 10 ),\n\t\t\t'sku'               => sanitize_title( $name ) . '-' . self::$faker->ean8,\n\t\t\t'global_unique_id'  => self::$faker->randomElement( array( self::$faker->ean13, self::$faker->isbn10 ) ),\n\t\t\t'attributes'        => $attributes,\n\t\t\t'tax_status'        => self::$faker->randomElement( array( 'taxable', 'shipping', 'none' ) ),\n\t\t\t'tax_class'         => '',\n\t\t\t'manage_stock'      => $will_manage_stock,\n\t\t\t'stock_quantity'    => $will_manage_stock ? self::$faker->numberBetween( -100, 100 ) : null,\n\t\t\t'stock_status'      => 'instock',\n\t\t\t'backorders'        => self::$faker->randomElement( array( 'yes', 'no', 'notify' ) ),\n\t\t\t'sold_individually' => self::$faker->boolean( 20 ),\n\t\t\t'upsell_ids'        => self::get_existing_product_ids(),\n\t\t\t'cross_sell_ids'    => self::get_existing_product_ids(),\n\t\t\t'image_id'          => self::get_image(),\n\t\t\t'category_ids'      => self::get_term_ids( 'product_cat', self::$faker->numberBetween( 0, 3 ) ),\n\t\t\t'tag_ids'           => self::get_term_ids( 'product_tag', self::$faker->numberBetween( 0, 5 ) ),\n\t\t\t'gallery_image_ids' => $gallery,\n\t\t\t'reviews_allowed'   => self::$faker->boolean(),\n\t\t\t'purchase_note'     => self::$faker->boolean() ? self::$faker->text() : '',\n\t\t\t'menu_order'        => self::$faker->numberBetween( 0, 10000 ),\n\t\t) );\n\t\t// Need to save to get an ID for variations.\n\t\t$product->save();\n\n\t\t// Create variations, one for each attribute value combination.\n\t\t$variation_attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' );\n\t\t$possible_attributes  = array_reverse( wc_array_cartesian( $variation_attributes ) );\n\t\tforeach ( $possible_attributes as $possible_attribute ) {\n\t\t\t$price             = self::$faker->randomFloat( 2, 1, 1000 );\n\t\t\t$is_on_sale        = self::$faker->boolean( 35 );\n\t\t\t$has_sale_schedule = $is_on_sale && self::$faker->boolean( 40 ); // ~40% of on-sale variations have a schedule.\n\t\t\t$sale_price        = $is_on_sale ? self::$faker->randomFloat( 2, 0, $price ) : '';\n\t\t\t$date_on_sale_from = $has_sale_schedule ? self::$faker->dateTimeBetween( '-3 days', '+3 days' )->format( DATE_ATOM ) : '';\n\t\t\t$date_on_sale_to   = $has_sale_schedule ? self::$faker->dateTimeBetween( '+4 days', '+4 months' )->format( DATE_ATOM ) : '';\n\t\t\t$is_virtual        = self::$faker->boolean( 20 );\n\t\t\t$variation         = new \\WC_Product_Variation();\n\t\t\t$variation->set_props( array(\n\t\t\t\t'parent_id'         => $product->get_id(),\n\t\t\t\t'attributes'        => $possible_attribute,\n\t\t\t\t'regular_price'     => $price,\n\t\t\t\t'sale_price'        => $sale_price,\n\t\t\t\t'date_on_sale_from' => $date_on_sale_from,\n\t\t\t\t'date_on_sale_to'   => $date_on_sale_to,\n\t\t\t\t'tax_status'        => self::$faker->randomElement( array( 'taxable', 'shipping', 'none' ) ),\n\t\t\t\t'tax_class'         => '',\n\t\t\t\t'manage_stock'      => $will_manage_stock,\n\t\t\t\t'stock_quantity'    => $will_manage_stock ? self::$faker->numberBetween( -20, 100 ) : null,\n\t\t\t\t'stock_status'      => 'instock',\n\t\t\t\t'weight'            => $is_virtual ? '' : self::$faker->numberBetween( 1, 200 ),\n\t\t\t\t'length'            => $is_virtual ? '' : self::$faker->numberBetween( 1, 200 ),\n\t\t\t\t'width'             => $is_virtual ? '' : self::$faker->numberBetween( 1, 200 ),\n\t\t\t\t'height'            => $is_virtual ? '' : self::$faker->numberBetween( 1, 200 ),\n\t\t\t\t'virtual'           => $is_virtual,\n\t\t\t\t'downloadable'      => false,\n\t\t\t\t'image_id'          => self::get_image(),\n\t\t\t) );\n\n\t\t\t// Set COGS if the feature is enabled.\n\t\t\tif ( wc_get_container()->get( 'Automattic\\WooCommerce\\Internal\\CostOfGoodsSold\\CostOfGoodsSoldController' )->feature_is_enabled() ) {\n\t\t\t\t$variation->set_props( array( 'cogs_value' => round( $price * ( 1 - self::$faker->numberBetween( 15, 60 ) / 100 ), 2 ) ) );\n\t\t\t}\n\n\t\t\t$variation->save();\n\t\t}\n\t\t$data_store = $product->get_data_store();\n\t\t$data_store->sort_all_product_variations( $product->get_id() );\n\n\t\treturn $product;\n\t}\n\n\t/**\n\t * Generate a simple product and return it.\n\t *\n\t * @return \\WC_Product\n\t */\n\tprotected static function generate_simple_product() {\n\t\t$name              = ucwords( self::$faker->productName );\n\t\t$will_manage_stock = self::$faker->boolean();\n\t\t$is_virtual        = self::$faker->boolean();\n\t\t$price             = self::$faker->randomFloat( 2, 1, 1000 );\n\t\t$is_on_sale        = self::$faker->boolean( 35 );\n\t\t$has_sale_schedule = $is_on_sale && self::$faker->boolean( 40 ); // ~40% scheduled, rest indefinite.\n\t\t$sale_price        = $is_on_sale ? self::$faker->randomFloat( 2, 0, $price ) : '';\n\t\t$date_on_sale_from = $has_sale_schedule ? self::$faker->dateTimeBetween( '-3 days', '+3 days' )->format( DATE_ATOM ) : '';\n\t\t$date_on_sale_to   = $has_sale_schedule ? self::$faker->dateTimeBetween( '+4 days', '+4 months' )->format( DATE_ATOM ) : '';\n\t\t$product           = new \\WC_Product();\n\n\t\t$image_id = self::get_image();\n\t\t$gallery  = self::maybe_get_gallery_image_ids();\n\n\t\t$product->set_props( array(\n\t\t\t'name'               => $name,\n\t\t\t'featured'           => self::$faker->boolean(),\n\t\t\t'catalog_visibility' => 'visible',\n\t\t\t'description'        => self::$faker->paragraphs( self::$faker->numberBetween( 1, 5 ), true ),\n\t\t\t'short_description'  => self::$faker->text(),\n\t\t\t'sku'                => sanitize_title( $name ) . '-' . self::$faker->ean8,\n\t\t\t'global_unique_id'   => self::$faker->randomElement( array( self::$faker->ean13, self::$faker->isbn10 ) ),\n\t\t\t'regular_price'      => $price,\n\t\t\t'sale_price'         => $sale_price,\n\t\t\t'date_on_sale_from'  => $date_on_sale_from,\n\t\t\t'date_on_sale_to'    => $date_on_sale_to,\n\t\t\t'total_sales'        => self::$faker->numberBetween( 0, 10000 ),\n\t\t\t'tax_status'         => self::$faker->randomElement( array( 'taxable', 'shipping', 'none' ) ),\n\t\t\t'tax_class'          => '',\n\t\t\t'manage_stock'       => $will_manage_stock,\n\t\t\t'stock_quantity'     => $will_manage_stock ? self::$faker->numberBetween( -100, 100 ) : null,\n\t\t\t'stock_status'       => 'instock',\n\t\t\t'backorders'         => self::$faker->randomElement( array( 'yes', 'no', 'notify' ) ),\n\t\t\t'sold_individually'  => self::$faker->boolean( 20 ),\n\t\t\t'weight'             => $is_virtual ? '' : self::$faker->numberBetween( 1, 200 ),\n\t\t\t'length'             => $is_virtual ? '' : self::$faker->numberBetween( 1, 200 ),\n\t\t\t'width'              => $is_virtual ? '' : self::$faker->numberBetween( 1, 200 ),\n\t\t\t'height'             => $is_virtual ? '' : self::$faker->numberBetween( 1, 200 ),\n\t\t\t'upsell_ids'         => self::get_existing_product_ids(),\n\t\t\t'cross_sell_ids'     => self::get_existing_product_ids(),\n\t\t\t'parent_id'          => 0,\n\t\t\t'reviews_allowed'    => self::$faker->boolean(),\n\t\t\t'purchase_note'      => self::$faker->boolean() ? self::$faker->text() : '',\n\t\t\t'menu_order'         => self::$faker->numberBetween( 0, 10000 ),\n\t\t\t'virtual'            => $is_virtual,\n\t\t\t'downloadable'       => false,\n\t\t\t'category_ids'       => self::get_term_ids( 'product_cat', self::$faker->numberBetween( 0, 3 ) ),\n\t\t\t'tag_ids'            => self::get_term_ids( 'product_tag', self::$faker->numberBetween( 0, 5 ) ),\n\t\t\t'shipping_class_id'  => 0,\n\t\t\t'image_id'           => $image_id,\n\t\t\t'gallery_image_ids'  => $gallery,\n\t\t) );\n\n\t\t// Set COGS if the feature is enabled.\n\t\tif ( wc_get_container()->get( 'Automattic\\WooCommerce\\Internal\\CostOfGoodsSold\\CostOfGoodsSoldController' )->feature_is_enabled() ) {\n\t\t\t$product->set_props( array( 'cogs_value' => round( $price * ( 1 - self::$faker->numberBetween( 15, 60 ) / 100 ), 2 ) ) );\n\t\t}\n\n\t\treturn $product;\n\t}\n\n\t/**\n\t * Maybe generate a number of terms for use with products, if there aren't enough existing terms.\n\t *\n\t * Number of terms is determined by the number of products that will be generated.\n\t *\n\t * @param int $product_amount The number of products that will be generated.\n\t *\n\t * @return void\n\t */\n\tprotected static function maybe_generate_terms( int $product_amount ): void {\n\t\tif ( $product_amount < 10 ) {\n\t\t\t$cats      = 5;\n\t\t\t$cat_depth = 1;\n\t\t\t$tags      = 10;\n\t\t\t$brands    = 5;\n\t\t} elseif ( $product_amount < 50 ) {\n\t\t\t$cats      = 10;\n\t\t\t$cat_depth = 2;\n\t\t\t$tags      = 20;\n\t\t\t$brands    = 10;\n\t\t} else {\n\t\t\t$cats      = 20;\n\t\t\t$cat_depth = 3;\n\t\t\t$tags      = 40;\n\t\t\t$brands    = 10;\n\t\t}\n\n\t\t$existing_cats = count( self::get_term_ids( 'product_cat', $cats ) );\n\t\tif ( $existing_cats < $cats ) {\n\t\t\tTerm::batch( $cats - $existing_cats, 'product_cat', array( 'max-depth' => $cat_depth ) );\n\t\t\tRandomRuntimeCache::clear( 'product_cat' );\n\t\t}\n\n\t\t$existing_tags = count( self::get_term_ids( 'product_tag', $tags ) );\n\t\tif ( $existing_tags < $tags ) {\n\t\t\tTerm::batch( $tags - $existing_tags, 'product_tag' );\n\t\t\tRandomRuntimeCache::clear( 'product_tag' );\n\t\t}\n\n\t\t$existing_brands = count( self::get_term_ids( 'product_brand', $brands ) );\n\t\tif ( $existing_brands < $brands ) {\n\t\t\tTerm::batch( $brands - $existing_brands, 'product_brand' );\n\t\t\tRandomRuntimeCache::clear( 'product_brand' );\n\t\t}\n\t}\n\n\t/**\n\t * Get a number of random term IDs for a specific taxonomy.\n\t *\n\t * @param string $taxonomy The taxonomy to get terms for.\n\t * @param int    $limit    The number of term IDs to get. Maximum value of 50.\n\t *\n\t * @return array\n\t */\n\tprotected static function get_term_ids( string $taxonomy, int $limit ): array {\n\t\tif ( $limit <= 0 ) {\n\t\t\treturn array();\n\t\t}\n\n\t\tif ( ! RandomRuntimeCache::exists( $taxonomy ) ) {\n\t\t\t$args = array(\n\t\t\t\t'taxonomy'   => $taxonomy,\n\t\t\t\t'number'     => 50,\n\t\t\t\t'orderby'    => 'count',\n\t\t\t\t'order'      => 'ASC',\n\t\t\t\t'hide_empty' => false,\n\t\t\t\t'fields'     => 'ids',\n\t\t\t);\n\n\t\t\tif ( 'product_cat' === $taxonomy ) {\n\t\t\t\t$uncategorized = get_term_by( 'slug', 'uncategorized', 'product_cat' );\n\t\t\t\tif ( $uncategorized ) {\n\t\t\t\t\t$args['exclude'] = $uncategorized->term_id;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$term_ids = get_terms( $args );\n\n\t\t\tRandomRuntimeCache::set( $taxonomy, $term_ids );\n\t\t}\n\n\t\tRandomRuntimeCache::shuffle( $taxonomy );\n\n\t\treturn RandomRuntimeCache::get( $taxonomy, $limit );\n\t}\n\n\t/**\n\t * Generate an image gallery.\n\t *\n\t * @return array\n\t */\n\tprotected static function maybe_get_gallery_image_ids() {\n\t\t$gallery = array();\n\n\t\t$create_gallery = self::$faker->boolean( 10 );\n\n\t\tif ( ! $create_gallery ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$image_count = wp_rand( 0, 3 );\n\n\t\tfor ( $i = 0; $i < $image_count; $i++ ) {\n\t\t\t$gallery[] = self::get_image();\n\t\t}\n\n\t\treturn $gallery;\n\t}\n\n\t/**\n\t * Get some random existing product IDs.\n\t *\n\t * @param int $limit Number of term IDs to get.\n\t * @return array\n\t */\n\tprotected static function get_existing_product_ids( $limit = 5 ) {\n\t\tif ( ! self::$product_ids ) {\n\t\t\tself::$product_ids = wc_get_products(\n\t\t\t\tarray(\n\t\t\t\t\t'limit'   => $limit,\n\t\t\t\t\t'return'  => 'ids',\n\t\t\t\t\t'status'  => 'publish',\n\t\t\t\t\t'orderby' => 'rand',\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t$random_limit = wp_rand( 0, $limit );\n\n\t\tif ( ! $random_limit ) {\n\t\t\treturn array();\n\t\t}\n\n\t\tshuffle( self::$product_ids );\n\n\t\treturn array_slice( self::$product_ids, 0, min( count( self::$product_ids ), $random_limit ) );\n\t}\n}\n"
  },
  {
    "path": "includes/Generator/Term.php",
    "content": "<?php\n/**\n * Generate taxonomy terms.\n *\n * @package SmoothGenerator\\Classes\n */\n\nnamespace WC\\SmoothGenerator\\Generator;\n\n/**\n * Customer data generator.\n */\nclass Term extends Generator {\n\t/**\n\t * Create a new taxonomy term.\n\t *\n\t * @param bool   $save     Whether to save the new term to the database.\n\t * @param string $taxonomy The taxonomy slug.\n\t * @param int    $parent   ID of parent term.\n\t *\n\t * @return \\WP_Error|\\WP_Term\n\t */\n\tpublic static function generate( $save = true, string $taxonomy = 'product_cat', int $parent = 0 ) {\n\t\t$taxonomy_obj = get_taxonomy( $taxonomy );\n\t\tif ( ! $taxonomy_obj ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_invalid_taxonomy',\n\t\t\t\t'The specified taxonomy is invalid.'\n\t\t\t);\n\t\t}\n\n\t\tif ( 0 !== $parent && true !== $taxonomy_obj->hierarchical ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_invalid_term_hierarchy',\n\t\t\t\t'The specified taxonomy does not support parent terms.'\n\t\t\t);\n\t\t}\n\n\t\tparent::maybe_initialize_generators();\n\n\t\tif ( 'product_brand' === $taxonomy ) {\n\t\t\t$term_name = self::$faker->company();\n\t\t} elseif ( $taxonomy_obj->hierarchical ) {\n\t\t\t$term_name = ucwords( self::$faker->department( 3 ) );\n\t\t} else {\n\t\t\t$term_name = self::random_weighted_element( array(\n\t\t\t\tself::$faker->lastName()       => 45,\n\t\t\t\tself::$faker->colorName()      => 35,\n\t\t\t\tself::$faker->words( 3, true ) => 20,\n\t\t\t) );\n\t\t\t$term_name = strtolower( $term_name );\n\t\t}\n\n\t\t$description_size = wp_rand( 20, 260 );\n\n\t\t$term_args = array(\n\t\t\t'description' => self::$faker->realTextBetween( $description_size, $description_size + 40, 4 ),\n\t\t);\n\t\tif ( 0 !== $parent ) {\n\t\t\t$term_args['parent'] = $parent;\n\t\t}\n\n\t\t$result = wp_insert_term( $term_name, $taxonomy, $term_args );\n\n\t\tif ( is_wp_error( $result ) ) {\n\t\t\treturn $result;\n\t\t}\n\n\t\t$term = get_term( $result['term_id'] );\n\n\t\t/**\n\t\t * Action: Term generator returned a new term.\n\t\t *\n\t\t * @since 1.1.0\n\t\t *\n\t\t * @param \\WP_Term $term\n\t\t */\n\t\tdo_action( 'smoothgenerator_term_generated', $term );\n\n\t\treturn $term;\n\t}\n\n\t/**\n\t * Create multiple terms for a taxonomy.\n\t *\n\t * @param int    $amount   The number of terms to create.\n\t * @param string $taxonomy The taxonomy to assign the terms to.\n\t * @param array  $args     Additional args for term creation.\n\t *\n\t * @return int[]|\\WP_Error\n\t */\n\tpublic static function batch( $amount, $taxonomy, array $args = array() ) {\n\t\t$amount = self::validate_batch_amount( $amount );\n\t\tif ( is_wp_error( $amount ) ) {\n\t\t\treturn $amount;\n\t\t}\n\n\t\t$taxonomy_obj = get_taxonomy( $taxonomy );\n\t\tif ( ! $taxonomy_obj ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_term_batch_invalid_taxonomy',\n\t\t\t\t'The specified taxonomy is invalid.'\n\t\t\t);\n\t\t}\n\n\t\tif ( true === $taxonomy_obj->hierarchical ) {\n\t\t\treturn self::batch_hierarchical( $amount, $taxonomy, $args );\n\t\t}\n\n\t\t$term_ids = array();\n\n\t\tfor ( $i = 1; $i <= $amount; $i++ ) {\n\t\t\t$term = self::generate( true, $taxonomy );\n\t\t\tif ( is_wp_error( $term ) ) {\n\t\t\t\tif ( 'term_exists' === $term->get_error_code() ) {\n\t\t\t\t\t--$i; // Try again.\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\treturn $term;\n\t\t\t}\n\t\t\t$term_ids[] = $term->term_id;\n\t\t}\n\n\t\treturn $term_ids;\n\t}\n\n\t/**\n\t * Create multiple terms for a hierarchical taxonomy.\n\t *\n\t * @param int    $amount   The number of terms to create.\n\t * @param string $taxonomy The taxonomy to assign the terms to.\n\t * @param array  $args     Additional args for term creation.\n\t *   @type int $max_depth The maximum level of hierarchy.\n\t *   @type int $parent    ID of a term to be the parent of the generated terms.\n\t *\n\t * @return int[]|\\WP_Error\n\t */\n\tprotected static function batch_hierarchical( int $amount, string $taxonomy, array $args = array() ) {\n\t\t$defaults = array(\n\t\t\t'max-depth' => 1,\n\t\t\t'parent'    => 0,\n\t\t);\n\n\t\tlist( 'max-depth' => $max_depth, 'parent' => $parent ) = filter_var_array(\n\t\t\twp_parse_args( $args, $defaults ),\n\t\t\tarray(\n\t\t\t\t'max-depth' => array(\n\t\t\t\t\t'filter'  => FILTER_VALIDATE_INT,\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'min_range' => 1,\n\t\t\t\t\t\t'max_range' => 5,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'parent'    => FILTER_VALIDATE_INT,\n\t\t\t)\n\t\t);\n\n\t\tif ( false === $max_depth ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_term_batch_invalid_max_depth',\n\t\t\t\t'Max depth must be a number between 1 and 5.'\n\t\t\t);\n\t\t}\n\t\tif ( false === $parent ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_term_batch_invalid_parent',\n\t\t\t\t'Parent must be the ID number of an existing term.'\n\t\t\t);\n\t\t}\n\n\t\t$term_ids = array();\n\n\t\tself::init_faker();\n\n\t\tif ( $parent || 1 === $max_depth ) {\n\t\t\t// All terms will be in the same hierarchy level.\n\t\t\tfor ( $i = 1; $i <= $amount; $i++ ) {\n\t\t\t\t$term = self::generate( true, $taxonomy, $parent );\n\t\t\t\tif ( is_wp_error( $term ) ) {\n\t\t\t\t\tif ( 'term_exists' === $term->get_error_code() ) {\n\t\t\t\t\t\t--$i; // Try again.\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn $term;\n\t\t\t\t}\n\t\t\t\t$term_ids[] = $term->term_id;\n\t\t\t}\n\t\t} else {\n\t\t\t$remaining = $amount;\n\t\t\t$term_max  = 1;\n\t\t\tif ( $amount > 2 ) {\n\t\t\t\t$term_max = floor( log( $amount ) );\n\t\t\t}\n\t\t\t$levels = array_fill( 1, $max_depth, array() );\n\n\t\t\tfor ( $i = 1; $i <= $max_depth; $i++ ) {\n\t\t\t\tif ( 1 === $i ) {\n\t\t\t\t\t// Always use the full term max for the top level of the hierarchy.\n\t\t\t\t\tfor ( $j = 1; $j <= $term_max && $remaining > 0; $j++ ) {\n\t\t\t\t\t\t$term = self::generate( true, $taxonomy );\n\t\t\t\t\t\tif ( is_wp_error( $term ) ) {\n\t\t\t\t\t\t\tif ( 'term_exists' === $term->get_error_code() ) {\n\t\t\t\t\t\t\t\t--$j; // Try again.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn $term;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t$term_ids[]     = $term->term_id;\n\t\t\t\t\t\t$levels[ $i ][] = $term->term_id;\n\t\t\t\t\t\t--$remaining;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Subsequent hierarchy levels.\n\t\t\t\t\tforeach ( $levels[ $i - 1 ] as $term_id ) {\n\t\t\t\t\t\t$tcount = wp_rand( 0, $term_max );\n\n\t\t\t\t\t\tfor ( $j = 1; $j <= $tcount && $remaining > 0; $j++ ) {\n\t\t\t\t\t\t\t$term = self::generate( true, $taxonomy, $term_id );\n\t\t\t\t\t\t\tif ( is_wp_error( $term ) ) {\n\t\t\t\t\t\t\t\tif ( 'term_exists' === $term->get_error_code() ) {\n\t\t\t\t\t\t\t\t\t--$j; // Try again.\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\treturn $term;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t$term_ids[]     = $term->term_id;\n\t\t\t\t\t\t\t$levels[ $i ][] = $term->term_id;\n\t\t\t\t\t\t\t--$remaining;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif ( $i === $max_depth && $remaining > 0 ) {\n\t\t\t\t\t// If we haven't generated enough yet, start back at the top level of the hierarchy.\n\t\t\t\t\t$i = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $term_ids;\n\t}\n}\n"
  },
  {
    "path": "includes/Plugin.php",
    "content": "<?php\n/**\n * Main plugin class.\n *\n * @package SmoothGenerator\\Classes\n */\n\nnamespace WC\\SmoothGenerator;\n\n/**\n * Main plugin class.\n */\nclass Plugin {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @param string $file Main plugin __FILE__ reference.\n\t */\n\tpublic function __construct( $file ) {\n\t\tif ( is_admin() ) {\n\t\t\tAdmin\\Settings::init();\n\t\t}\n\n\t\tif ( class_exists( 'WP_CLI' ) ) {\n\t\t\t$cli = new CLI();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/Router.php",
    "content": "<?php\n\nnamespace WC\\SmoothGenerator;\n\n/**\n * Methods to retrieve and use a particular generator class based on its slug.\n */\nclass Router {\n\t/**\n\t * @const array Associative array of available generator classes.\n\t */\n\tconst GENERATORS = array(\n\t\t'coupons'   => Generator\\Coupon::class,\n\t\t'customers' => Generator\\Customer::class,\n\t\t'orders'    => Generator\\Order::class,\n\t\t'products'  => Generator\\Product::class,\n\t\t'terms'     => Generator\\Term::class,\n\t);\n\n\t/**\n\t * Get the classname of a generator via slug.\n\t *\n\t * @param string $generator_slug The slug of the generator to retrieve.\n\t *\n\t * @return string|\\WP_Error\n\t */\n\tpublic static function get_generator_class( string $generator_slug ) {\n\t\tif ( ! isset( self::GENERATORS[ $generator_slug ] ) ) {\n\t\t\treturn new \\WP_Error(\n\t\t\t\t'smoothgenerator_invalid_generator',\n\t\t\t\tsprintf(\n\t\t\t\t\t'A generator class for \"%s\" can\\'t be found.',\n\t\t\t\t\t$generator_slug\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn self::GENERATORS[ $generator_slug ];\n\t}\n\n\t/**\n\t * Generate a batch of objects using the specified generator.\n\t *\n\t * @param string $generator_slug The slug identifier of the generator to use.\n\t * @param int    $amount         The number of objects to generate.\n\t * @param array  $args           Additional args for object generation.\n\t *\n\t * @return int[]|\\WP_Error\n\t */\n\tpublic static function generate_batch( string $generator_slug, int $amount, array $args = array() ) {\n\t\t$generator = self::get_generator_class( $generator_slug );\n\n\t\tif ( is_wp_error( $generator ) ) {\n\t\t\treturn $generator;\n\t\t}\n\n\t\treturn $generator::batch( $amount, $args );\n\t}\n}\n"
  },
  {
    "path": "includes/Util/RandomRuntimeCache.php",
    "content": "<?php\n/**\n * A runtime object cache for storing and randomly retrieving reusable data.\n *\n * @package SmoothGenerator\\Util\n */\n\nnamespace WC\\SmoothGenerator\\Util;\n\n/**\n * Class RandomRuntimeCache.\n */\nclass RandomRuntimeCache {\n\t/**\n\t * Associative array for storing groups of cache items.\n\t *\n\t * @var array\n\t */\n\tprivate static $cache = array();\n\n\t/**\n\t * Check if a specific cache group exists.\n\t *\n\t * @param string $group The specified cache group.\n\t *\n\t * @return bool\n\t */\n\tpublic static function exists( string $group ): bool {\n\t\treturn array_key_exists( $group, self::$cache );\n\t}\n\n\t/**\n\t * Get a number of items from a specific cache group.\n\t *\n\t * The retrieved items will be from the top of the group's array.\n\t *\n\t * @param string $group The specified cache group.\n\t * @param int    $limit Optional. Get up to this many items. Using 0 will return all the items in the group.\n\t *                      Default 0.\n\t *\n\t * @return array\n\t */\n\tpublic static function get( string $group, int $limit = 0 ): array {\n\t\t$all_items = self::get_group( $group );\n\n\t\tif ( $limit <= 0 || count( $all_items ) <= $limit ) {\n\t\t\treturn $all_items;\n\t\t}\n\n\t\t$items = array_slice( $all_items, 0, $limit );\n\n\t\treturn $items;\n\t}\n\n\t/**\n\t * Remove a number of items from a specific cache group and return them.\n\t *\n\t * The items will be extracted from the top of the group's array.\n\t *\n\t * @param string $group The specified cache group.\n\t * @param int    $limit Optional. Extract up to this many items. Using 0 will return all the items in the group and\n\t *                      delete it from the cache. Default 0.\n\t *\n\t * @return array\n\t */\n\tpublic static function extract( string $group, int $limit = 0 ): array {\n\t\t$all_items = self::get_group( $group );\n\n\t\tif ( $limit <= 0 || count( $all_items ) <= $limit ) {\n\t\t\tself::clear( $group );\n\n\t\t\treturn $all_items;\n\t\t}\n\n\t\t$items           = array_slice( $all_items, 0, $limit );\n\t\t$remaining_items = array_slice( $all_items, $limit );\n\n\t\tself::set( $group, $remaining_items );\n\n\t\treturn $items;\n\t}\n\n\t/**\n\t * Add items to a specific cache group.\n\t *\n\t * @param string $group The specified cache group.\n\t * @param array  $items The items to add to the group.\n\t *\n\t * @return void\n\t */\n\tpublic static function add( string $group, array $items ): void {\n\t\t$existing_items = self::get_group( $group );\n\n\t\tself::set( $group, array_merge( $existing_items, $items ) );\n\t}\n\n\t/**\n\t * Set a cache group to contain a specific set of items.\n\t *\n\t * @param string $group The specified cache group.\n\t * @param array  $items The items that will be in the group.\n\t *\n\t * @return void\n\t */\n\tpublic static function set( string $group, array $items ): void {\n\t\tself::$cache[ $group ] = $items;\n\t}\n\n\t/**\n\t * Count the number of items in a specific cache group.\n\t *\n\t * @param string $group The specified cache group.\n\t *\n\t * @return int\n\t */\n\tpublic static function count( string $group ): int {\n\t\t$group = self::get_group( $group );\n\n\t\treturn count( $group );\n\t}\n\n\t/**\n\t * Shuffle the order of the items in a specific cache group.\n\t *\n\t * @param string $group The specified cache group.\n\t *\n\t * @return void\n\t */\n\tpublic static function shuffle( string $group ): void {\n\t\t// Ensure group exists.\n\t\tself::get_group( $group );\n\n\t\tshuffle( self::$cache[ $group ] );\n\t}\n\n\t/**\n\t * Delete a group from the cache.\n\t *\n\t * @param string $group The specified cache group.\n\t *\n\t * @return void\n\t */\n\tpublic static function clear( string $group ): void {\n\t\tunset( self::$cache[ $group ] );\n\t}\n\n\t/**\n\t * Clear the entire cache.\n\t *\n\t * @return void\n\t */\n\tpublic static function reset(): void {\n\t\tself::$cache = array();\n\t}\n\n\t/**\n\t * Get the items in a cache group, ensuring that the group exists in the cache.\n\t *\n\t * @param string $group The specified cache group.\n\t *\n\t * @return array\n\t */\n\tprivate static function get_group( string $group ): array {\n\t\tif ( ! self::exists( $group ) ) {\n\t\t\tself::set( $group, array() );\n\t\t}\n\n\t\treturn self::$cache[ $group ];\n\t}\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"wc-smooth-generator\",\n  \"title\": \"WooCommerce Smooth Generator\",\n  \"version\": \"1.3.0\",\n  \"homepage\": \"https://github.com/woocommerce/wc-smooth-generator\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/woocommerce/wc-smooth-generator.git\"\n  },\n  \"license\": \"GPL-3.0+\",\n  \"scripts\": {\n    \"setup\": \"npm install && composer install && husky install\",\n    \"build\": \"composer install --no-dev && npm install --omit=dev && composer archive --file=$npm_package_name --format=zip && rm -rf build && mkdir -p build/$npm_package_name && unzip -q $npm_package_name.zip -d build/$npm_package_name && rm $npm_package_name.zip && cd build && zip -qr ../$npm_package_name.zip $npm_package_name && cd .. && rm -rf build\",\n    \"phpcs\": \"composer run phpcs\",\n    \"lint\": \"composer run lint\",\n    \"lint:staged\": \"composer run lint-staged\",\n    \"lint:branch\": \"composer run lint-branch\"\n  },\n  \"devDependencies\": {\n    \"husky\": \"^8.0.0\"\n  },\n  \"engines\": {\n    \"node\": \">=22\",\n    \"npm\": \">=6.4.1\"\n  }\n}\n"
  },
  {
    "path": "phpcs.xml.dist",
    "content": "<?xml version=\"1.0\"?>\n<ruleset name=\"WordPress Coding Standards\">\n\t<!-- See https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml -->\n\t<!-- See https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/blob/develop/WordPress-Core/ruleset.xml -->\n\n\t<description>WooCommerce dev PHP_CodeSniffer ruleset.</description>\n\n\t<file>.</file>\n\n\t<!-- Exclude paths -->\n\t<exclude-pattern>*/vendor/*</exclude-pattern>\n\t<exclude-pattern>*/node_modules/*</exclude-pattern>\n\n\t<!-- Show progress, show the error codes for each message (source). -->\n\t<arg value=\"ps\"/>\n\n\t<!-- Strip the filepaths down to the relevant bit. -->\n\t<arg name=\"basepath\" value=\"./\"/>\n\n\t<!-- Check up to 8 files simultaneously. -->\n\t<arg name=\"parallel\" value=\"8\"/>\n\n\t<!-- Configs -->\n\t<config name=\"minimum_supported_wp_version\" value=\"5.0\"/>\n\t<config name=\"testVersion\" value=\"7.1-\"/>\n\n\t<!-- Rules -->\n\t<rule ref=\"WooCommerce-Core\">\n\t\t<exclude name=\"WordPress.Files.FileName\"/>\n\t\t<exclude name=\"WordPress.NamingConventions.ValidVariableName\"/>\n\t\t<exclude name=\"WordPress.DateTime.RestrictedFunctions.date_date\"/>\n\n\t\t<exclude name=\"PEAR.Functions.FunctionCallSignature.CloseBracketLine\"/>\n\t\t<exclude name=\"PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket\"/>\n\t\t<exclude name=\"PEAR.Functions.FunctionCallSignature.MultipleArguments\"/>\n\t</rule>\n\t<rule ref=\"WordPress.WP.I18n\">\n\t\t<properties>\n\t\t\t<property name=\"text_domain\" type=\"array\" value=\"wc-smooth-generator\"/>\n\t\t</properties>\n\t</rule>\n</ruleset>\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<phpunit\n\t\tbootstrap=\"tests/bootstrap.php\"\n\t\tbackupGlobals=\"false\"\n\t\tcolors=\"true\"\n\t\tconvertErrorsToExceptions=\"true\"\n\t\tconvertNoticesToExceptions=\"true\"\n\t\tconvertWarningsToExceptions=\"true\"\n\t\tdefaultTestSuite=\"unit\"\n>\n\t<testsuites>\n\t\t<testsuite name=\"unit\">\n\t\t\t<directory suffix=\"Test.php\">./tests/Unit</directory>\n\t\t</testsuite>\n\t</testsuites>\n\t<coverage>\n\t\t<include>\n\t\t\t<directory suffix=\".php\">./includes</directory>\n\t\t</include>\n\t\t<exclude>\n\t\t\t<directory suffix=\".php\">./includes/CLI.php</directory>\n\t\t</exclude>\n\t</coverage>\n</phpunit>\n"
  },
  {
    "path": "tests/README.md",
    "content": "# WC Smooth Generator - Test Suite\n\nComprehensive unit test suite for the WooCommerce Smooth Generator plugin, targeting 90% code coverage.\n\n## Setup\n\n### Prerequisites\n\n1. Install PHP 7.4 or higher\n2. Install Composer\n3. Install WordPress test library\n4. Install WooCommerce\n\n### Installing WordPress Test Library\n\nRun the WordPress test installation script:\n\n```bash\nbash bin/install-wp-tests.sh wordpress_test root '' localhost latest\n```\n\nReplace the arguments as needed:\n- `wordpress_test` - Database name for tests\n- `root` - Database username\n- Empty string - Database password (or your password)\n- `localhost` - Database host\n- `latest` - WordPress version to install (or specific version like `6.4`)\n\n### Environment Variables\n\nSet these environment variables before running tests:\n\n```bash\nexport WP_TESTS_DIR=/path/to/wordpress-tests-lib\nexport WP_CORE_DIR=/path/to/wordpress\nexport WC_DIR=/path/to/woocommerce\n```\n\nOr create them temporarily:\n\n```bash\nWP_TESTS_DIR=/tmp/wordpress-tests-lib \\\nWP_CORE_DIR=/tmp/wordpress \\\nWC_DIR=/path/to/woocommerce \\\nvendor/bin/phpunit\n```\n\n## Running Tests\n\n### Run All Tests\n\n```bash\ncomposer test-unit\n```\n\nOr directly:\n\n```bash\nvendor/bin/phpunit\n```\n\n### Run Specific Test File\n\n```bash\nvendor/bin/phpunit tests/Unit/Generator/ProductTest.php\n```\n\n### Run Specific Test\n\n```bash\nvendor/bin/phpunit --filter test_generate_simple_product\n```\n\n### Run with Coverage Report\n\n```bash\nvendor/bin/phpunit --coverage-html coverage\n```\n\nThen open `coverage/index.html` in your browser.\n\n### Run Tests by Group\n\nTests can be organized with `@group` annotations:\n\n```bash\nvendor/bin/phpunit --group generator\nvendor/bin/phpunit --group slow --exclude-group slow\n```\n\n## Test Structure\n\n```\ntests/\n├── bootstrap.php              # Test environment setup\n├── README.md                  # This file\n└── Unit/\n    ├── Generator/             # Generator class tests\n    │   ├── GeneratorTest.php  # Base generator tests\n    │   ├── ProductTest.php    # Product generation tests\n    │   ├── OrderTest.php      # Order generation tests\n    │   ├── CustomerTest.php   # Customer generation tests\n    │   └── CouponTest.php     # Coupon generation tests\n    ├── Util/                  # Utility class tests\n    │   └── RandomRuntimeCacheTest.php\n    └── PluginTest.php         # Main plugin tests\n```\n\n## Test Coverage Goals\n\n- **Overall**: ~90% code coverage\n- **Generator Classes**: 85-95% coverage (core functionality)\n- **Utility Classes**: 95%+ coverage (simpler logic)\n- **Admin Classes**: 70-80% coverage (UI-heavy)\n\n## Writing Tests\n\n### Test File Template\n\n```php\n<?php\nnamespace WC\\SmoothGenerator\\Tests\\Generator;\n\nuse WC\\SmoothGenerator\\Generator\\YourClass;\nuse WP_UnitTestCase;\n\nclass YourClassTest extends WP_UnitTestCase {\n\n    public function setUp(): void {\n        parent::setUp();\n        // Setup code\n    }\n\n    public function tearDown(): void {\n        // Cleanup code\n        parent::tearDown();\n    }\n\n    public function test_your_functionality() {\n        // Arrange\n        $expected = 'value';\n\n        // Act\n        $result = YourClass::method();\n\n        // Assert\n        $this->assertEquals( $expected, $result );\n    }\n}\n```\n\n### Best Practices\n\n1. **Use descriptive test names**: `test_generate_simple_product_with_sale_price()`\n2. **Follow AAA pattern**: Arrange, Act, Assert\n3. **One assertion per test** (when practical)\n4. **Clean up after tests**: Use `tearDown()` to reset state\n5. **Use data providers** for parameterized tests\n6. **Mock external dependencies** when appropriate\n\n### Available Assertions\n\nPHPUnit provides many assertions. Common ones:\n\n- `$this->assertEquals( $expected, $actual )`\n- `$this->assertTrue( $condition )`\n- `$this->assertInstanceOf( ClassName::class, $object )`\n- `$this->assertNotEmpty( $value )`\n- `$this->assertWPError( $result )`\n- `$this->assertGreaterThan( $threshold, $value )`\n\nSee: https://docs.phpunit.de/en/9.6/assertions.html\n\n## Continuous Integration\n\nTests should be run on:\n- Every pull request\n- Before merging to main branch\n- Nightly builds\n\n## Troubleshooting\n\n### \"Class not found\" errors\n\nRegenerate the autoloader:\n\n```bash\ncomposer dump-autoload\n```\n\n### Database errors\n\nEnsure your test database is created and accessible:\n\n```bash\nmysql -u root -p -e \"CREATE DATABASE IF NOT EXISTS wordpress_test;\"\n```\n\n### \"Call to undefined function\" for WC functions\n\nEnsure WooCommerce path is set correctly:\n\n```bash\nexport WC_DIR=/path/to/woocommerce\n```\n\n### Permission errors on temp directories\n\n```bash\nchmod -R 777 /tmp/wordpress-tests-lib\n```\n\n## Resources\n\n- [PHPUnit Documentation](https://docs.phpunit.de/)\n- [WordPress Plugin Handbook - Unit Tests](https://developer.wordpress.org/plugins/testing/automated-testing/)\n- [WooCommerce Testing Guide](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment)\n"
  },
  {
    "path": "tests/Unit/Generator/CouponTest.php",
    "content": "<?php\n/**\n * Tests for Coupon Generator.\n *\n * @package WC\\SmoothGenerator\\Tests\\Generator\n */\n\nnamespace WC\\SmoothGenerator\\Tests\\Generator;\n\nuse WC\\SmoothGenerator\\Generator\\Coupon;\nuse WP_UnitTestCase;\n\n/**\n * Coupon Generator test case.\n */\nclass CouponTest extends WP_UnitTestCase {\n\n\t/**\n\t * Test generating a coupon.\n\t */\n\tpublic function test_generate_coupon() {\n\t\t$coupon = Coupon::generate( true );\n\n\t\t$this->assertInstanceOf( \\WC_Coupon::class, $coupon );\n\t\t$this->assertTrue( $coupon->get_id() > 0 );\n\t\t$this->assertNotEmpty( $coupon->get_code() );\n\t}\n\n\t/**\n\t * Test coupon has amount.\n\t */\n\tpublic function test_coupon_has_amount() {\n\t\t$coupon = Coupon::generate( true );\n\n\t\t$amount = $coupon->get_amount();\n\t\t$this->assertGreaterThan( 0, $amount );\n\t}\n\n\t/**\n\t * Test coupon with custom min and max.\n\t */\n\tpublic function test_coupon_custom_min_max() {\n\t\t$coupon = Coupon::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'min' => 10,\n\t\t\t\t'max' => 20,\n\t\t\t)\n\t\t);\n\n\t\t$amount = $coupon->get_amount();\n\t\t$this->assertGreaterThanOrEqual( 10, $amount );\n\t\t$this->assertLessThanOrEqual( 20, $amount );\n\t}\n\n\t/**\n\t * Test coupon with fixed_cart discount type.\n\t */\n\tpublic function test_coupon_fixed_cart_type() {\n\t\t$coupon = Coupon::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'discount_type' => 'fixed_cart',\n\t\t\t)\n\t\t);\n\n\t\t$this->assertEquals( 'fixed_cart', $coupon->get_discount_type() );\n\t}\n\n\t/**\n\t * Test coupon with percent discount type.\n\t */\n\tpublic function test_coupon_percent_type() {\n\t\t$coupon = Coupon::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'discount_type' => 'percent',\n\t\t\t)\n\t\t);\n\n\t\t$this->assertEquals( 'percent', $coupon->get_discount_type() );\n\t}\n\n\t/**\n\t * Test batch coupon generation.\n\t */\n\tpublic function test_batch_generation() {\n\t\t$amount     = 5;\n\t\t$coupon_ids = Coupon::batch( $amount );\n\n\t\t$this->assertIsArray( $coupon_ids );\n\t\t$this->assertCount( $amount, $coupon_ids );\n\n\t\tforeach ( $coupon_ids as $coupon_id ) {\n\t\t\t$coupon = new \\WC_Coupon( $coupon_id );\n\t\t\t$this->assertTrue( $coupon->get_id() > 0 );\n\t\t}\n\t}\n\n\t/**\n\t * Test batch validation.\n\t */\n\tpublic function test_batch_validation() {\n\t\t$result = Coupon::batch( 0 );\n\n\t\t$this->assertWPError( $result );\n\t}\n\n\t/**\n\t * Test invalid min value returns error.\n\t */\n\tpublic function test_invalid_min_value() {\n\t\t$coupon = Coupon::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'min' => -5,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPError( $coupon );\n\t}\n\n\t/**\n\t * Test invalid max value returns error.\n\t */\n\tpublic function test_invalid_max_value() {\n\t\t$coupon = Coupon::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'max' => 0,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPError( $coupon );\n\t}\n\n\t/**\n\t * Test min greater than max returns error.\n\t */\n\tpublic function test_min_greater_than_max() {\n\t\t$coupon = Coupon::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'min' => 50,\n\t\t\t\t'max' => 10,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPError( $coupon );\n\t}\n\n\t/**\n\t * Test invalid discount type returns error.\n\t */\n\tpublic function test_invalid_discount_type() {\n\t\t$coupon = Coupon::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'discount_type' => 'invalid_type',\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPError( $coupon );\n\t}\n\n\t/**\n\t * Test get_random returns false when no coupons exist.\n\t */\n\tpublic function test_get_random_no_coupons() {\n\t\t$coupon = Coupon::get_random();\n\n\t\t$this->assertFalse( $coupon );\n\t}\n\n\t/**\n\t * Test get_random returns coupon when coupons exist.\n\t */\n\tpublic function test_get_random_with_coupons() {\n\t\t// Create some coupons.\n\t\tCoupon::batch( 3 );\n\n\t\t$coupon = Coupon::get_random();\n\n\t\t$this->assertInstanceOf( \\WC_Coupon::class, $coupon );\n\t\t$this->assertTrue( $coupon->get_id() > 0 );\n\t}\n\n\t/**\n\t * Test coupon action hook is fired.\n\t */\n\tpublic function test_coupon_generated_action_hook() {\n\t\t$hook_fired = false;\n\t\t$generated_coupon = null;\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_coupon_generated',\n\t\t\tfunction ( $coupon ) use ( &$hook_fired, &$generated_coupon ) {\n\t\t\t\t$hook_fired = true;\n\t\t\t\t$generated_coupon = $coupon;\n\t\t\t}\n\t\t);\n\n\t\t$coupon = Coupon::generate( true );\n\n\t\t$this->assertTrue( $hook_fired, 'smoothgenerator_coupon_generated action should fire' );\n\t\t$this->assertInstanceOf( \\WC_Coupon::class, $generated_coupon );\n\t}\n\n\t/**\n\t * Test coupon code format.\n\t */\n\tpublic function test_coupon_code_format() {\n\t\t$coupon = Coupon::generate( true );\n\n\t\t$code = $coupon->get_code();\n\t\t$this->assertNotEmpty( $code );\n\t\t// Code should end with numbers (the amount).\n\t\t$this->assertMatchesRegularExpression( '/\\d+$/', $code );\n\t}\n}\n"
  },
  {
    "path": "tests/Unit/Generator/CustomerTest.php",
    "content": "<?php\n/**\n * Tests for Customer Generator.\n *\n * @package WC\\SmoothGenerator\\Tests\\Generator\n */\n\nnamespace WC\\SmoothGenerator\\Tests\\Generator;\n\nuse WC\\SmoothGenerator\\Generator\\Customer;\nuse WP_UnitTestCase;\n\n/**\n * Customer Generator test case.\n */\nclass CustomerTest extends WP_UnitTestCase {\n\n\t/**\n\t * Test generating a customer.\n\t */\n\tpublic function test_generate_customer() {\n\t\t$customer = Customer::generate( true );\n\n\t\t$this->assertInstanceOf( \\WC_Customer::class, $customer );\n\t\t$this->assertTrue( $customer->get_id() > 0 );\n\t}\n\n\t/**\n\t * Test customer has billing information.\n\t */\n\tpublic function test_customer_has_billing_info() {\n\t\t$customer = Customer::generate( true, array( 'type' => 'person', 'country' => 'US' ) );\n\n\t\t$this->assertNotEmpty( $customer->get_billing_first_name(), 'Billing first name should not be empty' );\n\t\t$this->assertNotEmpty( $customer->get_billing_last_name(), 'Billing last name should not be empty' );\n\t\t$this->assertNotEmpty( $customer->get_billing_email(), 'Billing email should not be empty' );\n\t\t$this->assertNotEmpty( $customer->get_billing_city(), 'Billing city should not be empty' );\n\t\t$this->assertNotEmpty( $customer->get_billing_country(), 'Billing country should not be empty' );\n\n\t\t// Address line 1 may be empty in some locales, so just check it's a string.\n\t\t$this->assertIsString( $customer->get_billing_address_1() );\n\t}\n\n\t/**\n\t * Test customer email is valid.\n\t */\n\tpublic function test_customer_email_valid() {\n\t\t$customer = Customer::generate( true );\n\n\t\t$email = $customer->get_billing_email();\n\t\t$this->assertNotFalse( filter_var( $email, FILTER_VALIDATE_EMAIL ) );\n\t}\n\n\t/**\n\t * Test customer with specific country.\n\t */\n\tpublic function test_customer_with_specific_country() {\n\t\t$customer = Customer::generate( true, array( 'country' => 'US' ) );\n\n\t\t$this->assertEquals( 'US', $customer->get_billing_country() );\n\t}\n\n\t/**\n\t * Test customer type person.\n\t */\n\tpublic function test_customer_type_person() {\n\t\t$customer = Customer::generate( true, array( 'type' => 'person' ) );\n\n\t\t$this->assertNotEmpty( $customer->get_billing_first_name() );\n\t\t$this->assertNotEmpty( $customer->get_billing_last_name() );\n\t}\n\n\t/**\n\t * Test customer type company.\n\t */\n\tpublic function test_customer_type_company() {\n\t\t$customer = Customer::generate( true, array( 'type' => 'company' ) );\n\n\t\t$this->assertNotEmpty( $customer->get_billing_company() );\n\t}\n\n\t/**\n\t * Test batch customer generation.\n\t */\n\tpublic function test_batch_generation() {\n\t\t$amount       = 5;\n\t\t$customer_ids = Customer::batch( $amount );\n\n\t\t$this->assertIsArray( $customer_ids );\n\t\t$this->assertCount( $amount, $customer_ids );\n\n\t\tforeach ( $customer_ids as $customer_id ) {\n\t\t\t$customer = new \\WC_Customer( $customer_id );\n\t\t\t$this->assertTrue( $customer->get_id() > 0 );\n\t\t}\n\t}\n\n\t/**\n\t * Test batch validation.\n\t */\n\tpublic function test_batch_validation() {\n\t\t$result = Customer::batch( 0 );\n\n\t\t$this->assertWPError( $result );\n\t}\n\n\t/**\n\t * Test customer action hook is fired.\n\t */\n\tpublic function test_customer_generated_action_hook() {\n\t\t$hook_fired = false;\n\t\t$generated_customer = null;\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_customer_generated',\n\t\t\tfunction ( $customer ) use ( &$hook_fired, &$generated_customer ) {\n\t\t\t\t$hook_fired = true;\n\t\t\t\t$generated_customer = $customer;\n\t\t\t}\n\t\t);\n\n\t\t$customer = Customer::generate( true );\n\n\t\t$this->assertTrue( $hook_fired, 'smoothgenerator_customer_generated action should fire' );\n\t\t$this->assertInstanceOf( \\WC_Customer::class, $generated_customer );\n\t}\n\n\t/**\n\t * Test customer with invalid country code returns error.\n\t */\n\tpublic function test_customer_with_invalid_country() {\n\t\t$customer = Customer::generate( true, array( 'country' => 'XX' ) );\n\n\t\t$this->assertWPError( $customer, 'Invalid country code should return WP_Error' );\n\t}\n\n\t/**\n\t * Test customer has phone number.\n\t */\n\tpublic function test_customer_has_phone() {\n\t\t$customer = Customer::generate( true );\n\n\t\t$phone = $customer->get_billing_phone();\n\t\t$this->assertNotEmpty( $phone );\n\t}\n\n\t/**\n\t * Test customer role is set to customer.\n\t */\n\tpublic function test_customer_role() {\n\t\t$customer = Customer::generate( true );\n\n\t\t$user = get_user_by( 'id', $customer->get_id() );\n\t\t$this->assertNotFalse( $user );\n\t\t$this->assertTrue( in_array( 'customer', $user->roles, true ) );\n\t}\n}\n"
  },
  {
    "path": "tests/Unit/Generator/GeneratorTest.php",
    "content": "<?php\n/**\n * Tests for Generator base class.\n *\n * @package WC\\SmoothGenerator\\Tests\\Generator\n */\n\nnamespace WC\\SmoothGenerator\\Tests\\Generator;\n\nuse WC\\SmoothGenerator\\Generator\\Product;\nuse WP_UnitTestCase;\n\n/**\n * Generator base class test case.\n */\nclass GeneratorTest extends WP_UnitTestCase {\n\n\t/**\n\t * Test MAX_BATCH_SIZE constant.\n\t */\n\tpublic function test_max_batch_size_constant() {\n\t\t$this->assertEquals( 100, Product::MAX_BATCH_SIZE );\n\t}\n\n\t/**\n\t * Test IMAGE_SIZE constant.\n\t */\n\tpublic function test_image_size_constant() {\n\t\t$this->assertEquals( 700, Product::IMAGE_SIZE );\n\t}\n\n\t/**\n\t * Test batch validation with max size.\n\t */\n\tpublic function test_batch_validation_max_size() {\n\t\t$result = Product::batch( Product::MAX_BATCH_SIZE, array( 'type' => 'simple' ) );\n\n\t\t$this->assertIsArray( $result );\n\t}\n\n\t/**\n\t * Test emails are disabled during generation.\n\t */\n\tpublic function test_emails_disabled() {\n\t\t// Generate a product to trigger initialization.\n\t\tProduct::generate( true, array( 'type' => 'simple' ) );\n\n\t\t// Check that the filter that blocks emails is in place or that emails are otherwise disabled.\n\t\t// The disable_emails method may run after this test, so we just verify the function exists.\n\t\t$this->assertTrue( method_exists( Product::class, 'disable_emails' ), 'disable_emails method should exist' );\n\t}\n\n\t/**\n\t * Test queued transactional emails are blocked.\n\t */\n\tpublic function test_queued_emails_blocked() {\n\t\t// Generate a product to trigger initialization.\n\t\tProduct::generate( true, array( 'type' => 'simple' ) );\n\n\t\t// The disable_emails function is called during generation.\n\t\t// We can test that it adds the filter by manually calling it.\n\t\tProduct::disable_emails();\n\n\t\t// Now check if the filter blocks emails.\n\t\t$result = apply_filters( 'woocommerce_allow_send_queued_transactional_email', true, null, null );\n\t\t$this->assertFalse( $result, 'Queued transactional emails should be blocked after calling disable_emails' );\n\t}\n}\n"
  },
  {
    "path": "tests/Unit/Generator/OrderTest.php",
    "content": "<?php\n/**\n * Tests for Order Generator.\n *\n * @package WC\\SmoothGenerator\\Tests\\Generator\n */\n\nnamespace WC\\SmoothGenerator\\Tests\\Generator;\n\nuse WC\\SmoothGenerator\\Generator\\Order;\nuse WC\\SmoothGenerator\\Generator\\Product;\nuse WC\\SmoothGenerator\\Generator\\Customer;\nuse WP_UnitTestCase;\n\n/**\n * Order Generator test case.\n */\nclass OrderTest extends WP_UnitTestCase {\n\n\t/**\n\t * Set up test fixtures.\n\t */\n\tpublic function setUp(): void {\n\t\tparent::setUp();\n\n\t\t// Create some products for orders to use.\n\t\tProduct::batch( 5, array( 'type' => 'simple' ) );\n\t}\n\n\t/**\n\t * Test generating a basic order.\n\t */\n\tpublic function test_generate_order() {\n\t\t$order = Order::generate( true );\n\n\t\t$this->assertInstanceOf( \\WC_Order::class, $order );\n\t\t$this->assertTrue( $order->get_id() > 0 );\n\t\t$this->assertEquals( 'smooth-generator', $order->get_created_via() );\n\t}\n\n\t/**\n\t * Test order has products.\n\t */\n\tpublic function test_order_has_products() {\n\t\t$order = Order::generate( true );\n\n\t\t$items = $order->get_items();\n\t\t$this->assertNotEmpty( $items, 'Order should have line items' );\n\n\t\tforeach ( $items as $item ) {\n\t\t\t$this->assertInstanceOf( \\WC_Order_Item_Product::class, $item );\n\t\t\t$this->assertGreaterThan( 0, $item->get_quantity() );\n\t\t}\n\t}\n\n\t/**\n\t * Test order with completed status.\n\t */\n\tpublic function test_order_completed_status() {\n\t\t$order = Order::generate( true, array( 'status' => 'completed' ) );\n\n\t\t$this->assertEquals( 'completed', $order->get_status() );\n\t\t$this->assertNotNull( $order->get_date_paid() );\n\t\t$this->assertNotNull( $order->get_date_completed() );\n\t}\n\n\t/**\n\t * Test order with processing status.\n\t */\n\tpublic function test_order_processing_status() {\n\t\t$order = Order::generate( true, array( 'status' => 'processing' ) );\n\n\t\t$this->assertEquals( 'processing', $order->get_status() );\n\t\t$this->assertNotNull( $order->get_date_paid() );\n\t}\n\n\t/**\n\t * Test order with failed status.\n\t */\n\tpublic function test_order_failed_status() {\n\t\t$order = Order::generate( true, array( 'status' => 'failed' ) );\n\n\t\t$this->assertEquals( 'failed', $order->get_status() );\n\t}\n\n\t/**\n\t * Test order with on-hold status.\n\t */\n\tpublic function test_order_on_hold_status() {\n\t\t$order = Order::generate( true, array( 'status' => 'on-hold' ) );\n\n\t\t$this->assertEquals( 'on-hold', $order->get_status() );\n\t}\n\n\t/**\n\t * Test order has customer information.\n\t */\n\tpublic function test_order_has_customer_info() {\n\t\t$order = Order::generate( true );\n\n\t\t// Country, email and name may be empty in some customer generation scenarios.\n\t\t// Just verify they return strings (even if empty).\n\t\t$this->assertIsString( $order->get_billing_country() );\n\t\t$this->assertIsString( $order->get_billing_email() );\n\t\t$this->assertIsString( $order->get_billing_first_name() );\n\t\t$this->assertIsString( $order->get_billing_last_name() );\n\n\t\t// At least verify that if email exists, it's valid.\n\t\t$email = $order->get_billing_email();\n\t\tif ( ! empty( $email ) ) {\n\t\t\t$this->assertNotFalse( filter_var( $email, FILTER_VALIDATE_EMAIL ), 'Email should be valid if present' );\n\t\t}\n\n\t\t// The important thing is that the order was successfully created.\n\t\t// Customer data population depends on various WooCommerce configuration and may be optional.\n\t\t$this->assertInstanceOf( \\WC_Order::class, $order );\n\t}\n\n\t/**\n\t * Test order has shipping information.\n\t */\n\tpublic function test_order_has_shipping_info() {\n\t\t$order = Order::generate( true );\n\n\t\t// Shipping country should be set.\n\t\t$shipping_country = $order->get_shipping_country();\n\t\t$this->assertIsString( $shipping_country );\n\t\tif ( ! empty( $shipping_country ) ) {\n\t\t\t$this->assertNotEmpty( $shipping_country );\n\t\t} else {\n\t\t\t$this->assertTrue( true, 'Shipping info may be empty in some configurations' );\n\t\t}\n\t}\n\n\t/**\n\t * Test order total is calculated.\n\t */\n\tpublic function test_order_total_calculated() {\n\t\t$order = Order::generate( true );\n\n\t\t$total = $order->get_total();\n\t\t$this->assertGreaterThan( 0, $total );\n\t}\n\n\t/**\n\t * Test batch order generation.\n\t */\n\tpublic function test_batch_generation() {\n\t\t$amount    = 5;\n\t\t$order_ids = Order::batch( $amount );\n\n\t\t$this->assertIsArray( $order_ids );\n\t\t$this->assertCount( $amount, $order_ids );\n\n\t\tforeach ( $order_ids as $order_id ) {\n\t\t\t$order = wc_get_order( $order_id );\n\t\t\t$this->assertInstanceOf( \\WC_Order::class, $order );\n\t\t}\n\t}\n\n\t/**\n\t * Test batch validation.\n\t */\n\tpublic function test_batch_validation() {\n\t\t$result = Order::batch( 0 );\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertEquals( 'smoothgenerator_batch_invalid_amount', $result->get_error_code() );\n\t}\n\n\t/**\n\t * Test order with specific date.\n\t */\n\tpublic function test_order_with_date() {\n\t\t$date = '2024-01-15';\n\t\t$order = Order::generate( true, array( 'date-start' => $date, 'date-end' => $date ) );\n\n\t\t$created_date = $order->get_date_created()->format( 'Y-m-d' );\n\t\t$this->assertEquals( $date, $created_date );\n\t}\n\n\t/**\n\t * Test order with date range.\n\t */\n\tpublic function test_order_with_date_range() {\n\t\t$start_date = '2024-01-01';\n\t\t$end_date   = '2024-01-31';\n\n\t\t$order = Order::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'date-start' => $start_date,\n\t\t\t\t'date-end'   => $end_date,\n\t\t\t)\n\t\t);\n\n\t\t$created_date = $order->get_date_created()->format( 'Y-m-d' );\n\t\t$this->assertGreaterThanOrEqual( $start_date, $created_date );\n\t\t$this->assertLessThanOrEqual( $end_date, $created_date );\n\t}\n\n\t/**\n\t * Test order with coupon using coupon-ratio.\n\t */\n\tpublic function test_order_with_coupon_ratio() {\n\t\t// Create some coupons first.\n\t\t$coupon = new \\WC_Coupon();\n\t\t$coupon->set_code( 'test-coupon-123' );\n\t\t$coupon->set_amount( 5 );\n\t\t$coupon->set_discount_type( 'fixed_cart' );\n\t\t$coupon->save();\n\n\t\t// Set the coupons flag to ensure at least one coupon exists.\n\t\t$order = Order::generate( true, array( 'coupon-ratio' => 1.0, 'coupons' => true ) );\n\n\t\t$coupons = $order->get_coupon_codes();\n\t\t// Note: Coupon application may fail if order total is less than coupon amount or other validation fails.\n\t\tif ( empty( $coupons ) ) {\n\t\t\t$this->markTestIncomplete( 'Coupon was not applied - may be due to validation or order total issues' );\n\t\t}\n\t\t$this->assertNotEmpty( $coupons, 'Order should have a coupon with ratio 1.0' );\n\t}\n\n\t/**\n\t * Test order action hook is fired.\n\t */\n\tpublic function test_order_generated_action_hook() {\n\t\t$hook_fired = false;\n\t\t$generated_order = null;\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_order_generated',\n\t\t\tfunction ( $order ) use ( &$hook_fired, &$generated_order ) {\n\t\t\t\t$hook_fired = true;\n\t\t\t\t$generated_order = $order;\n\t\t\t}\n\t\t);\n\n\t\t$order = Order::generate( true );\n\n\t\t$this->assertTrue( $hook_fired, 'smoothgenerator_order_generated action should fire' );\n\t\t$this->assertInstanceOf( \\WC_Order::class, $generated_order );\n\t}\n\n\t/**\n\t * Test completed order dates are sequential.\n\t */\n\tpublic function test_completed_order_dates_sequential() {\n\t\t$order = Order::generate( true, array( 'status' => 'completed' ) );\n\n\t\t$date_created   = $order->get_date_created()->getTimestamp();\n\t\t$date_paid      = $order->get_date_paid()->getTimestamp();\n\t\t$date_completed = $order->get_date_completed()->getTimestamp();\n\n\t\t$this->assertLessThanOrEqual( $date_paid, $date_created );\n\t\t$this->assertLessThanOrEqual( $date_completed, $date_paid );\n\t}\n\n\t/**\n\t * Test order with refund using refund-ratio.\n\t */\n\tpublic function test_order_with_refund_ratio() {\n\t\t$order = Order::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'status'       => 'completed',\n\t\t\t\t'refund-ratio' => 1.0,\n\t\t\t)\n\t\t);\n\n\t\t$refunds = $order->get_refunds();\n\t\t$this->assertNotEmpty( $refunds, 'Order should have refund with ratio 1.0' );\n\t}\n\n\t/**\n\t * Test full refund changes order status to refunded.\n\t */\n\tpublic function test_full_refund_status() {\n\t\t$order = Order::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'status'       => 'completed',\n\t\t\t\t'refund-ratio' => 1.0,\n\t\t\t)\n\t\t);\n\n\t\t// Check if any orders are fully refunded (they should be with ratio 1.0).\n\t\t$refunded_amount = $order->get_total_refunded();\n\t\tif ( abs( $refunded_amount - $order->get_total() ) < 0.01 ) {\n\t\t\t$this->assertEquals( 'refunded', $order->get_status() );\n\t\t}\n\t}\n\n\t/**\n\t * Test partial refund doesn't change order status.\n\t */\n\tpublic function test_partial_refund_status() {\n\t\t// Generate orders and check for partial refunds.\n\t\t$found_partial = false;\n\t\tfor ( $i = 0; $i < 5; $i++ ) {\n\t\t\t$order = Order::generate(\n\t\t\t\ttrue,\n\t\t\t\tarray(\n\t\t\t\t\t'status'       => 'completed',\n\t\t\t\t\t'refund-ratio' => 0.5,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$refunds = $order->get_refunds();\n\t\t\tif ( ! empty( $refunds ) ) {\n\t\t\t\t$refunded_amount = $order->get_total_refunded();\n\t\t\t\t$order_total = $order->get_total();\n\n\t\t\t\t// If it's a partial refund (not full).\n\t\t\t\tif ( $refunded_amount > 0 && $refunded_amount < $order_total ) {\n\t\t\t\t\t$this->assertEquals( 'completed', $order->get_status() );\n\t\t\t\t\t$found_partial = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// If we didn't find a partial refund, that's still OK - just ensure the test always has an assertion.\n\t\t$this->assertTrue( true, 'Test completed - partial refunds are probabilistic with 0.5 ratio' );\n\t}\n\n\t/**\n\t * Test batch orders have exact coupon distribution with coupon-ratio.\n\t */\n\tpublic function test_batch_exact_coupon_distribution() {\n\t\t// Create a coupon first.\n\t\t$coupon = new \\WC_Coupon();\n\t\t$coupon->set_code( 'batch-test-coupon' );\n\t\t$coupon->set_amount( 10 );\n\t\t$coupon->set_discount_type( 'fixed_cart' );\n\t\t$coupon->save();\n\n\t\t$amount       = 10;\n\t\t$coupon_ratio = 0.5;\n\t\t$order_ids    = Order::batch(\n\t\t\t$amount,\n\t\t\tarray(\n\t\t\t\t'coupon-ratio' => $coupon_ratio,\n\t\t\t)\n\t\t);\n\n\t\t// Count orders with coupons.\n\t\t$coupon_count = 0;\n\t\tforeach ( $order_ids as $order_id ) {\n\t\t\t$order = wc_get_order( $order_id );\n\t\t\tif ( count( $order->get_coupon_codes() ) > 0 ) {\n\t\t\t\t$coupon_count++;\n\t\t\t}\n\t\t}\n\n\t\t// Should have exactly 5 orders with coupons (50% of 10).\n\t\t$expected_count = (int) round( $amount * $coupon_ratio );\n\t\t$this->assertEquals( $expected_count, $coupon_count, 'Should have exact coupon distribution in batch' );\n\t}\n\n\t/**\n\t * Test batch orders have exact refund distribution with refund-ratio.\n\t */\n\tpublic function test_batch_exact_refund_distribution() {\n\t\t$amount       = 20;\n\t\t$refund_ratio = 0.5;\n\t\t$order_ids    = Order::batch(\n\t\t\t$amount,\n\t\t\tarray(\n\t\t\t\t'status'       => 'completed',\n\t\t\t\t'refund-ratio' => $refund_ratio,\n\t\t\t)\n\t\t);\n\n\t\t// Count orders with refunds.\n\t\t$refund_count = 0;\n\t\tforeach ( $order_ids as $order_id ) {\n\t\t\t$order = wc_get_order( $order_id );\n\t\t\tif ( count( $order->get_refunds() ) > 0 ) {\n\t\t\t\t$refund_count++;\n\t\t\t}\n\t\t}\n\n\t\t// Should have exactly 10 orders with refunds (50% of 20).\n\t\t$expected_count = (int) round( $amount * $refund_ratio );\n\t\t$this->assertEquals( $expected_count, $refund_count, 'Should have exact refund distribution in batch' );\n\t}\n\n\t/**\n\t * Test refund has proper line items.\n\t */\n\tpublic function test_refund_has_line_items() {\n\t\t$order = Order::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'status'       => 'completed',\n\t\t\t\t'refund-ratio' => 1.0,\n\t\t\t)\n\t\t);\n\n\t\t$refunds = $order->get_refunds();\n\t\tif ( ! empty( $refunds ) ) {\n\t\t\t$refund = $refunds[0];\n\t\t\t$items  = $refund->get_items();\n\t\t\t$this->assertNotEmpty( $items, 'Refund should have line items' );\n\t\t}\n\t}\n\n\t/**\n\t * Test refund amount is valid.\n\t */\n\tpublic function test_refund_amount_valid() {\n\t\t$order = Order::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'status'       => 'completed',\n\t\t\t\t'refund-ratio' => 1.0,\n\t\t\t)\n\t\t);\n\n\t\t$refunds = $order->get_refunds();\n\t\tif ( ! empty( $refunds ) ) {\n\t\t\t$refund_amount = $order->get_total_refunded();\n\t\t\t$this->assertGreaterThan( 0, $refund_amount );\n\t\t\t$this->assertLessThanOrEqual( $order->get_total(), $refund_amount );\n\t\t}\n\t}\n\n\t/**\n\t * Test batch orders with date range.\n\t */\n\tpublic function test_batch_orders_chronological_dates() {\n\t\t$start_timestamp = strtotime( '2024-01-01' );\n\t\t$end_timestamp   = strtotime( '2024-01-31 23:59:59' );\n\n\t\t$order_ids = Order::batch(\n\t\t\t5,\n\t\t\tarray(\n\t\t\t\t'date-start' => '2024-01-01',\n\t\t\t\t'date-end'   => '2024-01-31',\n\t\t\t)\n\t\t);\n\n\t\t$dates = array();\n\t\tforeach ( $order_ids as $order_id ) {\n\t\t\t$order   = wc_get_order( $order_id );\n\t\t\t$timestamp = $order->get_date_created()->getTimestamp();\n\t\t\t$dates[] = $timestamp;\n\n\t\t\t// Verify each date is within the specified range.\n\t\t\t$this->assertGreaterThanOrEqual( $start_timestamp, $timestamp, 'Order date should be after start date' );\n\t\t\t$this->assertLessThanOrEqual( $end_timestamp, $timestamp, 'Order date should be before end date' );\n\t\t}\n\n\t\t// Verify we got the expected number of orders.\n\t\t$this->assertCount( 5, $dates, 'Should generate 5 orders' );\n\t}\n\n\t/**\n\t * Test order sometimes has fees.\n\t */\n\tpublic function test_order_with_fees() {\n\t\t$found_fee = false;\n\t\t// Try multiple times to find an order with fees (20% probability).\n\t\tfor ( $i = 0; $i < 20; $i++ ) {\n\t\t\t$order = Order::generate( true );\n\t\t\t$fees  = $order->get_fees();\n\t\t\tif ( ! empty( $fees ) ) {\n\t\t\t\t$found_fee = true;\n\t\t\t\tforeach ( $fees as $fee ) {\n\t\t\t\t\t$this->assertInstanceOf( \\WC_Order_Item_Fee::class, $fee );\n\t\t\t\t\t$this->assertGreaterThan( 0, $fee->get_total() );\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t$this->assertTrue( $found_fee, 'Should generate at least one order with fees in 20 attempts' );\n\t}\n\n\t/**\n\t * Test order has valid currency.\n\t */\n\tpublic function test_order_has_currency() {\n\t\t$order = Order::generate( true );\n\n\t\t$currency = $order->get_currency();\n\t\t$this->assertEquals( get_woocommerce_currency(), $currency );\n\t}\n\n\t/**\n\t * Test refund dates are after order completion.\n\t */\n\tpublic function test_refund_dates_after_completion() {\n\t\t$order = Order::generate(\n\t\t\ttrue,\n\t\t\tarray(\n\t\t\t\t'status'       => 'completed',\n\t\t\t\t'refund-ratio' => 1.0,\n\t\t\t\t'date-start'   => '2024-01-01',\n\t\t\t\t'date-end'     => '2024-01-01',\n\t\t\t)\n\t\t);\n\n\t\t$refunds = $order->get_refunds();\n\t\tif ( ! empty( $refunds ) ) {\n\t\t\t$refund = $refunds[0];\n\t\t\t$order_completed = $order->get_date_completed()->getTimestamp();\n\t\t\t$refund_created  = $refund->get_date_created()->getTimestamp();\n\n\t\t\t// Allow for same timestamp (within 1 second) since refund can happen immediately after completion.\n\t\t\t$this->assertGreaterThanOrEqual( $order_completed - 1, $refund_created, 'Refund date should be at or after order completion' );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "tests/Unit/Generator/ProductTest.php",
    "content": "<?php\n/**\n * Tests for Product Generator.\n *\n * @package WC\\SmoothGenerator\\Tests\\Generator\n */\n\nnamespace WC\\SmoothGenerator\\Tests\\Generator;\n\nuse WC\\SmoothGenerator\\Generator\\Product;\nuse WP_UnitTestCase;\n\n/**\n * Product Generator test case.\n */\nclass ProductTest extends WP_UnitTestCase {\n\n\t/**\n\t * Test generating a simple product.\n\t */\n\tpublic function test_generate_simple_product() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$this->assertInstanceOf( \\WC_Product::class, $product );\n\t\t$this->assertTrue( $product->get_id() > 0 );\n\t\t$this->assertEquals( 'simple', $product->get_type() );\n\t\t$this->assertNotEmpty( $product->get_name() );\n\t\t$this->assertNotEmpty( $product->get_sku() );\n\t\t$this->assertGreaterThan( 0, $product->get_regular_price() );\n\t}\n\n\t/**\n\t * Test generating a variable product.\n\t */\n\tpublic function test_generate_variable_product() {\n\t\t$product = Product::generate( true, array( 'type' => 'variable' ) );\n\n\t\t// Check if product generation returned an error.\n\t\tif ( is_wp_error( $product ) ) {\n\t\t\t$this->markTestSkipped( 'Variable product generation failed: ' . $product->get_error_message() );\n\t\t}\n\n\t\t// Skip if product is not the right type or doesn't have an ID.\n\t\tif ( ! $product instanceof \\WC_Product_Variable || ! $product->get_id() ) {\n\t\t\t$this->markTestSkipped( 'Variable product generation failed' );\n\t\t}\n\n\t\t$this->assertInstanceOf( \\WC_Product_Variable::class, $product );\n\t\t$this->assertTrue( $product->get_id() > 0 );\n\t\t$this->assertEquals( 'variable', $product->get_type() );\n\t\t$this->assertNotEmpty( $product->get_name() );\n\n\t\t// Check that variations were created (refresh product to get updated data).\n\t\t$product    = wc_get_product( $product->get_id() );\n\t\t$variations = $product->get_children();\n\t\t// Note: Variations may not be created if attribute registration fails in test environment.\n\t\t// This is a known limitation of the test setup.\n\t\tif ( empty( $variations ) ) {\n\t\t\t$this->markTestSkipped( 'Variations not created - attribute registration may have failed in test environment' );\n\t\t}\n\t\t$this->assertNotEmpty( $variations, 'Variable product should have variations' );\n\t}\n\n\t/**\n\t * Test that variable products have attributes.\n\t */\n\tpublic function test_variable_product_has_attributes() {\n\t\t$product = Product::generate( true, array( 'type' => 'variable' ) );\n\n\t\t// Check if product generation returned an error.\n\t\tif ( is_wp_error( $product ) ) {\n\t\t\t$this->markTestSkipped( 'Variable product generation failed: ' . $product->get_error_message() );\n\t\t}\n\n\t\t// Skip if product generation had issues.\n\t\tif ( ! $product || ! $product->get_id() ) {\n\t\t\t$this->markTestSkipped( 'Variable product generation failed' );\n\t\t}\n\n\t\t$attributes = $product->get_attributes();\n\n\t\t// Skip if attribute registration failed.\n\t\tif ( empty( $attributes ) ) {\n\t\t\t$this->markTestSkipped( 'No attributes created - attribute registration may have failed in test environment' );\n\t\t}\n\n\t\t$this->assertNotEmpty( $attributes, 'Variable product should have attributes' );\n\n\t\tforeach ( $attributes as $attribute ) {\n\t\t\t$this->assertInstanceOf( \\WC_Product_Attribute::class, $attribute );\n\t\t\t$this->assertNotEmpty( $attribute->get_name() );\n\t\t\t$this->assertNotEmpty( $attribute->get_options() );\n\t\t}\n\t}\n\n\t/**\n\t * Test that variations have proper prices.\n\t */\n\tpublic function test_variations_have_prices() {\n\t\t$product = Product::generate( true, array( 'type' => 'variable' ) );\n\n\t\t// Check if product generation returned an error.\n\t\tif ( is_wp_error( $product ) ) {\n\t\t\t$this->markTestSkipped( 'Variable product generation failed: ' . $product->get_error_message() );\n\t\t}\n\n\t\t// Skip if product generation had issues.\n\t\tif ( ! $product || ! $product->get_id() ) {\n\t\t\t$this->markTestSkipped( 'Variable product generation failed' );\n\t\t}\n\n\t\t// Refresh product to get variations.\n\t\t$product    = wc_get_product( $product->get_id() );\n\t\t$variations = $product->get_children();\n\n\t\tif ( empty( $variations ) ) {\n\t\t\t$this->markTestSkipped( 'No variations created - attribute registration may have failed' );\n\t\t}\n\n\t\tforeach ( $variations as $variation_id ) {\n\t\t\t$variation = wc_get_product( $variation_id );\n\t\t\t$this->assertInstanceOf( \\WC_Product_Variation::class, $variation );\n\t\t\t$this->assertGreaterThan( 0, $variation->get_regular_price() );\n\t\t}\n\t}\n\n\t/**\n\t * Test batch product generation.\n\t */\n\tpublic function test_batch_generation() {\n\t\t$amount      = 5;\n\t\t$product_ids = Product::batch( $amount, array( 'type' => 'simple' ) );\n\n\t\t$this->assertIsArray( $product_ids );\n\t\t$this->assertCount( $amount, $product_ids );\n\n\t\tforeach ( $product_ids as $product_id ) {\n\t\t\t$product = wc_get_product( $product_id );\n\t\t\t$this->assertInstanceOf( \\WC_Product::class, $product );\n\t\t\t$this->assertTrue( $product->get_id() > 0 );\n\t\t}\n\t}\n\n\t/**\n\t * Test batch validation with invalid amount.\n\t */\n\tpublic function test_batch_validation_invalid_amount() {\n\t\t$result = Product::batch( 0 );\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertEquals( 'smoothgenerator_batch_invalid_amount', $result->get_error_code() );\n\t}\n\n\t/**\n\t * Test batch validation with amount too large.\n\t */\n\tpublic function test_batch_validation_amount_too_large() {\n\t\t$result = Product::batch( 150 );\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertEquals( 'smoothgenerator_batch_invalid_amount', $result->get_error_code() );\n\t}\n\n\t/**\n\t * Test that products have categories assigned.\n\t */\n\tpublic function test_products_have_categories() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$category_ids = $product->get_category_ids();\n\t\t// Categories are randomly assigned (0-3), so we just check it's an array.\n\t\t$this->assertIsArray( $category_ids );\n\t}\n\n\t/**\n\t * Test that products have tags assigned.\n\t */\n\tpublic function test_products_have_tags() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$tag_ids = $product->get_tag_ids();\n\t\t// Tags are randomly assigned (0-5), so we just check it's an array.\n\t\t$this->assertIsArray( $tag_ids );\n\t}\n\n\t/**\n\t * Test that products have images.\n\t */\n\tpublic function test_products_have_images() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$image_id = $product->get_image_id();\n\t\t$this->assertGreaterThan( 0, $image_id, 'Product should have an image' );\n\n\t\t// Check if image generation worked in test environment.\n\t\t// Image generation may fail in some test setups due to GD library availability.\n\t\tif ( $image_id > 0 ) {\n\t\t\t$this->assertTrue( true, 'Product has image ID' );\n\t\t}\n\t}\n\n\t/**\n\t * Test product with sale price.\n\t */\n\tpublic function test_product_sale_price() {\n\t\t// Generate multiple products to increase chance of getting one on sale.\n\t\t$found_sale = false;\n\t\tfor ( $i = 0; $i < 20; $i++ ) {\n\t\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\t\t\tif ( $product->is_on_sale() ) {\n\t\t\t\t$found_sale = true;\n\t\t\t\t$this->assertGreaterThan( 0, $product->get_sale_price() );\n\t\t\t\t$this->assertLessThan( $product->get_regular_price(), $product->get_sale_price() );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t$this->assertTrue( $found_sale, 'Should generate at least one product on sale in 20 attempts' );\n\t}\n\n\t/**\n\t * Test product stock management.\n\t */\n\tpublic function test_product_stock_management() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t// Stock management is random, so we just verify the values make sense.\n\t\tif ( $product->managing_stock() ) {\n\t\t\t$this->assertIsNumeric( $product->get_stock_quantity() );\n\t\t} else {\n\t\t\t// If not managing stock, verify it's set to false.\n\t\t\t$this->assertFalse( $product->managing_stock() );\n\t\t}\n\t}\n\n\t/**\n\t * Test product with upsells.\n\t */\n\tpublic function test_product_upsells() {\n\t\t// Create some simple products first.\n\t\tProduct::batch( 5, array( 'type' => 'simple' ) );\n\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$upsell_ids = $product->get_upsell_ids();\n\t\t$this->assertIsArray( $upsell_ids );\n\t}\n\n\t/**\n\t * Test product with cross-sells.\n\t */\n\tpublic function test_product_cross_sells() {\n\t\t// Create some simple products first.\n\t\tProduct::batch( 5, array( 'type' => 'simple' ) );\n\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$cross_sell_ids = $product->get_cross_sell_ids();\n\t\t$this->assertIsArray( $cross_sell_ids );\n\t}\n\n\t/**\n\t * Test that product has valid tax status.\n\t */\n\tpublic function test_product_tax_status() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$tax_status = $product->get_tax_status();\n\t\t$this->assertContains( $tax_status, array( 'taxable', 'shipping', 'none' ) );\n\t}\n\n\t/**\n\t * Test simple product dimensions.\n\t */\n\tpublic function test_simple_product_dimensions() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t// Non-virtual products should have dimensions.\n\t\tif ( ! $product->is_virtual() ) {\n\t\t\t$weight = $product->get_weight();\n\t\t\tif ( ! empty( $weight ) ) {\n\t\t\t\t$this->assertGreaterThan( 0, $weight );\n\t\t\t} else {\n\t\t\t\t// If weight is empty, that's still valid for physical products.\n\t\t\t\t$this->assertIsString( $weight, 'Weight should be a string even if empty' );\n\t\t\t}\n\t\t} else {\n\t\t\t// Virtual products shouldn't have weight.\n\t\t\t$this->assertTrue( $product->is_virtual() );\n\t\t}\n\t}\n\n\t/**\n\t * Test virtual products don't have dimensions.\n\t */\n\tpublic function test_virtual_product_no_dimensions() {\n\t\t$found_virtual = false;\n\t\t// Try multiple times to find a virtual product.\n\t\tfor ( $i = 0; $i < 20; $i++ ) {\n\t\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\t\t\tif ( $product->is_virtual() ) {\n\t\t\t\t$found_virtual = true;\n\t\t\t\t$this->assertEmpty( $product->get_weight() );\n\t\t\t\t$this->assertEmpty( $product->get_length() );\n\t\t\t\t$this->assertEmpty( $product->get_width() );\n\t\t\t\t$this->assertEmpty( $product->get_height() );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t$this->assertTrue( $found_virtual, 'Should generate at least one virtual product in 20 attempts' );\n\t}\n\n\t/**\n\t * Test product reviews allowed.\n\t */\n\tpublic function test_product_reviews_allowed() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$this->assertIsBool( $product->get_reviews_allowed() );\n\t}\n\n\t/**\n\t * Test product backorders setting.\n\t */\n\tpublic function test_product_backorders() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$backorders = $product->get_backorders();\n\t\t$this->assertContains( $backorders, array( 'yes', 'no', 'notify' ) );\n\t}\n\n\t/**\n\t * Test product action hook is fired.\n\t */\n\tpublic function test_product_generated_action_hook() {\n\t\t$hook_fired        = false;\n\t\t$generated_product = null;\n\n\t\tadd_action(\n\t\t\t'smoothgenerator_product_generated',\n\t\t\tfunction ( $product ) use ( &$hook_fired, &$generated_product ) {\n\t\t\t\t$hook_fired        = true;\n\t\t\t\t$generated_product = $product;\n\t\t\t}\n\t\t);\n\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$this->assertTrue( $hook_fired, 'smoothgenerator_product_generated action should fire' );\n\t\t$this->assertInstanceOf( \\WC_Product::class, $generated_product );\n\t\t$this->assertEquals( $product->get_id(), $generated_product->get_id() );\n\t}\n\n\t/**\n\t * Test product with global unique ID.\n\t */\n\tpublic function test_product_global_unique_id() {\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t$global_unique_id = $product->get_global_unique_id();\n\t\t$this->assertNotEmpty( $global_unique_id, 'Product should have a global unique ID' );\n\t}\n\n\t/**\n\t * Test batch generation with use-existing-terms flag.\n\t */\n\tpublic function test_batch_with_existing_terms() {\n\t\t// Create some terms first.\n\t\twp_insert_term( 'Test Category', 'product_cat' );\n\t\twp_insert_term( 'Test Tag', 'product_tag' );\n\n\t\t$product_ids = Product::batch( 3, array(\n\t\t\t'use-existing-terms' => true,\n\t\t\t'type'               => 'simple',\n\t\t) );\n\n\t\t$this->assertIsArray( $product_ids );\n\t\t$this->assertCount( 3, $product_ids );\n\t}\n\n\t/**\n\t * Test variation sale prices.\n\t */\n\tpublic function test_variation_sale_prices() {\n\t\t$product = Product::generate( true, array( 'type' => 'variable' ) );\n\n\t\t// Check if product generation returned an error.\n\t\tif ( is_wp_error( $product ) ) {\n\t\t\t$this->markTestSkipped( 'Variable product generation failed: ' . $product->get_error_message() );\n\t\t}\n\n\t\t// Skip if product generation had issues.\n\t\tif ( ! $product || ! $product->get_id() ) {\n\t\t\t$this->markTestSkipped( 'Variable product generation failed' );\n\t\t}\n\n\t\t// Refresh product to get variations.\n\t\t$product    = wc_get_product( $product->get_id() );\n\t\t$variations = $product->get_children();\n\n\t\tif ( empty( $variations ) ) {\n\t\t\t$this->markTestSkipped( 'No variations created - attribute registration may have failed' );\n\t\t}\n\n\t\t$found_sale = false;\n\n\t\tforeach ( $variations as $variation_id ) {\n\t\t\t$variation = wc_get_product( $variation_id );\n\t\t\tif ( $variation->is_on_sale() ) {\n\t\t\t\t$found_sale = true;\n\t\t\t\t$this->assertGreaterThan( 0, $variation->get_sale_price() );\n\t\t\t\t$this->assertLessThan( $variation->get_regular_price(), $variation->get_sale_price() );\n\t\t\t}\n\t\t}\n\n\t\t// With probability, at least some variations should be on sale.\n\t\t$this->assertTrue( count( $variations ) > 0, 'Should have at least one variation' );\n\t}\n\n\t/**\n\t * Test featured products.\n\t */\n\tpublic function test_featured_products() {\n\t\t$found_featured = false;\n\t\t// Try multiple times to find a featured product (10% probability) - use simple products.\n\t\tfor ( $i = 0; $i < 30; $i++ ) {\n\t\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\t\t\tif ( $product->get_featured() ) {\n\t\t\t\t$found_featured = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t$this->assertTrue( $found_featured, 'Should generate at least one featured product in 30 attempts' );\n\t}\n\n\t/**\n\t * Test that products have brands assigned when taxonomy exists.\n\t */\n\tpublic function test_products_have_brands_when_taxonomy_exists() {\n\t\t// Register product_brand taxonomy for the test.\n\t\tregister_taxonomy(\n\t\t\t'product_brand',\n\t\t\t'product',\n\t\t\tarray(\n\t\t\t\t'labels'       => array( 'name' => 'Brands' ),\n\t\t\t\t'hierarchical' => false,\n\t\t\t\t'show_ui'      => true,\n\t\t\t\t'query_var'    => true,\n\t\t\t\t'rewrite'      => array( 'slug' => 'brand' ),\n\t\t\t)\n\t\t);\n\n\t\t// Create some brand terms.\n\t\twp_insert_term( 'Test Brand 1', 'product_brand' );\n\t\twp_insert_term( 'Test Brand 2', 'product_brand' );\n\t\twp_insert_term( 'Test Brand 3', 'product_brand' );\n\n\t\t// Clear the cache to ensure fresh term lookup.\n\t\t\\WC\\SmoothGenerator\\Util\\RandomRuntimeCache::clear( 'product_brand' );\n\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t// Get assigned brands.\n\t\t$brand_terms = wp_get_object_terms( $product->get_id(), 'product_brand' );\n\n\t\t$this->assertIsArray( $brand_terms );\n\t\t$this->assertGreaterThanOrEqual( 1, count( $brand_terms ), 'Product should have at least 1 brand' );\n\t\t$this->assertLessThanOrEqual( 3, count( $brand_terms ), 'Product should have at most 3 brands' );\n\n\t\t// Clean up.\n\t\tunregister_taxonomy( 'product_brand' );\n\t}\n\n\t/**\n\t * Test that product generation doesn't fail when brand taxonomy doesn't exist.\n\t */\n\tpublic function test_product_generation_without_brand_taxonomy() {\n\t\t// Ensure taxonomy doesn't exist.\n\t\tif ( taxonomy_exists( 'product_brand' ) ) {\n\t\t\tunregister_taxonomy( 'product_brand' );\n\t\t}\n\n\t\t$product = Product::generate( true, array( 'type' => 'simple' ) );\n\n\t\t// Should still generate successfully.\n\t\t$this->assertInstanceOf( \\WC_Product::class, $product );\n\t\t$this->assertTrue( $product->get_id() > 0 );\n\t\t$this->assertEquals( 'simple', $product->get_type() );\n\n\t\t// Should have no brand terms.\n\t\t$brand_terms = wp_get_object_terms( $product->get_id(), 'product_brand' );\n\t\t$this->assertTrue( is_wp_error( $brand_terms ) || empty( $brand_terms ) );\n\t}\n}\n"
  },
  {
    "path": "tests/Unit/PluginTest.php",
    "content": "<?php\n/**\n * Tests for Plugin main class.\n *\n * @package WC\\SmoothGenerator\\Tests\n */\n\nnamespace WC\\SmoothGenerator\\Tests;\n\nuse WC\\SmoothGenerator\\Plugin;\nuse WP_UnitTestCase;\n\n/**\n * Plugin test case.\n */\nclass PluginTest extends WP_UnitTestCase {\n\n\t/**\n\t * Test plugin can be instantiated.\n\t */\n\tpublic function test_plugin_instantiation() {\n\t\t$plugin = new Plugin( __FILE__ );\n\n\t\t$this->assertInstanceOf( Plugin::class, $plugin );\n\t}\n}\n"
  },
  {
    "path": "tests/Unit/Util/RandomRuntimeCacheTest.php",
    "content": "<?php\n/**\n * Tests for RandomRuntimeCache utility class.\n *\n * @package WC\\SmoothGenerator\\Tests\\Util\n */\n\nnamespace WC\\SmoothGenerator\\Tests\\Util;\n\nuse WC\\SmoothGenerator\\Util\\RandomRuntimeCache;\nuse WP_UnitTestCase;\n\n/**\n * RandomRuntimeCache test case.\n */\nclass RandomRuntimeCacheTest extends WP_UnitTestCase {\n\n\t/**\n\t * Reset the cache before each test.\n\t */\n\tpublic function setUp(): void {\n\t\tparent::setUp();\n\t\tRandomRuntimeCache::reset();\n\t}\n\n\t/**\n\t * Reset the cache after each test.\n\t */\n\tpublic function tearDown(): void {\n\t\tRandomRuntimeCache::reset();\n\t\tparent::tearDown();\n\t}\n\n\t/**\n\t * Test that exists returns false for non-existent group.\n\t */\n\tpublic function test_exists_returns_false_for_non_existent_group() {\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'test_group' ) );\n\t}\n\n\t/**\n\t * Test that exists returns true after setting a group.\n\t */\n\tpublic function test_exists_returns_true_after_set() {\n\t\tRandomRuntimeCache::set( 'test_group', array( 1, 2, 3 ) );\n\t\t$this->assertTrue( RandomRuntimeCache::exists( 'test_group' ) );\n\t}\n\n\t/**\n\t * Test setting and getting items.\n\t */\n\tpublic function test_set_and_get_items() {\n\t\t$items = array( 1, 2, 3, 4, 5 );\n\t\tRandomRuntimeCache::set( 'test_group', $items );\n\n\t\t$result = RandomRuntimeCache::get( 'test_group' );\n\n\t\t$this->assertEquals( $items, $result );\n\t}\n\n\t/**\n\t * Test getting items with limit.\n\t */\n\tpublic function test_get_with_limit() {\n\t\t$items = array( 1, 2, 3, 4, 5 );\n\t\tRandomRuntimeCache::set( 'test_group', $items );\n\n\t\t$result = RandomRuntimeCache::get( 'test_group', 3 );\n\n\t\t$this->assertCount( 3, $result );\n\t\t$this->assertEquals( array( 1, 2, 3 ), $result );\n\t}\n\n\t/**\n\t * Test getting items with limit larger than available.\n\t */\n\tpublic function test_get_with_limit_larger_than_available() {\n\t\t$items = array( 1, 2, 3 );\n\t\tRandomRuntimeCache::set( 'test_group', $items );\n\n\t\t$result = RandomRuntimeCache::get( 'test_group', 10 );\n\n\t\t$this->assertCount( 3, $result );\n\t\t$this->assertEquals( $items, $result );\n\t}\n\n\t/**\n\t * Test getting items with zero limit returns all items.\n\t */\n\tpublic function test_get_with_zero_limit_returns_all() {\n\t\t$items = array( 1, 2, 3, 4, 5 );\n\t\tRandomRuntimeCache::set( 'test_group', $items );\n\n\t\t$result = RandomRuntimeCache::get( 'test_group', 0 );\n\n\t\t$this->assertEquals( $items, $result );\n\t}\n\n\t/**\n\t * Test extracting items removes them from cache.\n\t */\n\tpublic function test_extract_removes_items_from_cache() {\n\t\t$items = array( 1, 2, 3, 4, 5 );\n\t\tRandomRuntimeCache::set( 'test_group', $items );\n\n\t\t$extracted = RandomRuntimeCache::extract( 'test_group', 3 );\n\n\t\t$this->assertEquals( array( 1, 2, 3 ), $extracted );\n\n\t\t$remaining = RandomRuntimeCache::get( 'test_group' );\n\t\t$this->assertEquals( array( 4, 5 ), $remaining );\n\t}\n\n\t/**\n\t * Test extracting all items deletes the group.\n\t */\n\tpublic function test_extract_all_deletes_group() {\n\t\t$items = array( 1, 2, 3 );\n\t\tRandomRuntimeCache::set( 'test_group', $items );\n\n\t\t$extracted = RandomRuntimeCache::extract( 'test_group', 0 );\n\n\t\t$this->assertEquals( $items, $extracted );\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'test_group' ) );\n\t}\n\n\t/**\n\t * Test extracting with limit larger than available.\n\t */\n\tpublic function test_extract_with_limit_larger_than_available() {\n\t\t$items = array( 1, 2, 3 );\n\t\tRandomRuntimeCache::set( 'test_group', $items );\n\n\t\t$extracted = RandomRuntimeCache::extract( 'test_group', 10 );\n\n\t\t$this->assertEquals( $items, $extracted );\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'test_group' ) );\n\t}\n\n\t/**\n\t * Test adding items to existing group.\n\t */\n\tpublic function test_add_items_to_existing_group() {\n\t\tRandomRuntimeCache::set( 'test_group', array( 1, 2, 3 ) );\n\t\tRandomRuntimeCache::add( 'test_group', array( 4, 5 ) );\n\n\t\t$result = RandomRuntimeCache::get( 'test_group' );\n\n\t\t$this->assertEquals( array( 1, 2, 3, 4, 5 ), $result );\n\t}\n\n\t/**\n\t * Test adding items to non-existent group creates it.\n\t */\n\tpublic function test_add_items_to_non_existent_group() {\n\t\tRandomRuntimeCache::add( 'test_group', array( 1, 2, 3 ) );\n\n\t\t$this->assertTrue( RandomRuntimeCache::exists( 'test_group' ) );\n\t\t$this->assertEquals( array( 1, 2, 3 ), RandomRuntimeCache::get( 'test_group' ) );\n\t}\n\n\t/**\n\t * Test counting items in group.\n\t */\n\tpublic function test_count_items() {\n\t\tRandomRuntimeCache::set( 'test_group', array( 1, 2, 3, 4, 5 ) );\n\n\t\t$count = RandomRuntimeCache::count( 'test_group' );\n\n\t\t$this->assertEquals( 5, $count );\n\t}\n\n\t/**\n\t * Test counting non-existent group returns zero.\n\t */\n\tpublic function test_count_non_existent_group() {\n\t\t$count = RandomRuntimeCache::count( 'test_group' );\n\n\t\t$this->assertEquals( 0, $count );\n\t}\n\n\t/**\n\t * Test shuffle randomizes items order.\n\t */\n\tpublic function test_shuffle_randomizes_order() {\n\t\t$items = range( 1, 100 );\n\t\tRandomRuntimeCache::set( 'test_group', $items );\n\n\t\tRandomRuntimeCache::shuffle( 'test_group' );\n\n\t\t$result = RandomRuntimeCache::get( 'test_group' );\n\n\t\t// Items should be same but likely in different order.\n\t\t$this->assertEquals( 100, count( $result ) );\n\t\t$this->assertEquals( array_sum( $items ), array_sum( $result ) );\n\t\t// Very unlikely to be in same order after shuffle (probability: 1/100!).\n\t\t$is_shuffled = false;\n\t\tfor ( $i = 0; $i < count( $items ); $i++ ) {\n\t\t\tif ( $items[ $i ] !== $result[ $i ] ) {\n\t\t\t\t$is_shuffled = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t$this->assertTrue( $is_shuffled, 'Items should be shuffled' );\n\t}\n\n\t/**\n\t * Test shuffle on non-existent group creates it.\n\t */\n\tpublic function test_shuffle_non_existent_group() {\n\t\tRandomRuntimeCache::shuffle( 'test_group' );\n\n\t\t$this->assertTrue( RandomRuntimeCache::exists( 'test_group' ) );\n\t\t$this->assertEquals( array(), RandomRuntimeCache::get( 'test_group' ) );\n\t}\n\n\t/**\n\t * Test clearing a group removes it.\n\t */\n\tpublic function test_clear_removes_group() {\n\t\tRandomRuntimeCache::set( 'test_group', array( 1, 2, 3 ) );\n\t\tRandomRuntimeCache::clear( 'test_group' );\n\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'test_group' ) );\n\t}\n\n\t/**\n\t * Test clearing non-existent group doesn't error.\n\t */\n\tpublic function test_clear_non_existent_group() {\n\t\tRandomRuntimeCache::clear( 'test_group' );\n\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'test_group' ) );\n\t}\n\n\t/**\n\t * Test reset clears all groups.\n\t */\n\tpublic function test_reset_clears_all_groups() {\n\t\tRandomRuntimeCache::set( 'group1', array( 1, 2, 3 ) );\n\t\tRandomRuntimeCache::set( 'group2', array( 4, 5, 6 ) );\n\t\tRandomRuntimeCache::set( 'group3', array( 7, 8, 9 ) );\n\n\t\tRandomRuntimeCache::reset();\n\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'group1' ) );\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'group2' ) );\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'group3' ) );\n\t}\n\n\t/**\n\t * Test multiple operations in sequence.\n\t */\n\tpublic function test_complex_operations_sequence() {\n\t\t// Set initial items.\n\t\tRandomRuntimeCache::set( 'test_group', array( 1, 2, 3, 4, 5 ) );\n\t\t$this->assertEquals( 5, RandomRuntimeCache::count( 'test_group' ) );\n\n\t\t// Extract some items.\n\t\t$extracted = RandomRuntimeCache::extract( 'test_group', 2 );\n\t\t$this->assertEquals( array( 1, 2 ), $extracted );\n\t\t$this->assertEquals( 3, RandomRuntimeCache::count( 'test_group' ) );\n\n\t\t// Add more items.\n\t\tRandomRuntimeCache::add( 'test_group', array( 6, 7 ) );\n\t\t$this->assertEquals( 5, RandomRuntimeCache::count( 'test_group' ) );\n\n\t\t// Get with limit.\n\t\t$result = RandomRuntimeCache::get( 'test_group', 3 );\n\t\t$this->assertCount( 3, $result );\n\n\t\t// Clear group.\n\t\tRandomRuntimeCache::clear( 'test_group' );\n\t\t$this->assertFalse( RandomRuntimeCache::exists( 'test_group' ) );\n\t}\n}\n"
  },
  {
    "path": "tests/bootstrap.php",
    "content": "<?php\n/**\n * Bootstrap file for PHPUnit tests.\n *\n * @package WC\\SmoothGenerator\\Tests\n */\n\ndeclare( strict_types=1 );\n\nnamespace WC\\SmoothGenerator\\Tests;\n\nuse WC_Install;\n\ndefine( 'PLUGIN_TESTS_DIR', __DIR__ );\n\nglobal $plugin_dir;\nglobal $wp_plugins_dir;\nglobal $wc_dir;\n\n$wp_tests_dir = getenv( 'WP_TESTS_DIR' ) ?: path_join( sys_get_temp_dir(), '/wordpress-tests-lib' );\nvalidate_file_exists( \"{$wp_tests_dir}/includes/functions.php\" );\n\n$wp_core_dir    = getenv( 'WP_CORE_DIR' ) ?: path_join( sys_get_temp_dir(), '/wordpress' );\n$wp_plugins_dir = path_join( $wp_core_dir, '/wp-content/plugins' );\n\n$plugin_dir = dirname( __DIR__ );\n\n$wc_dir = getenv( 'WC_DIR' );\nif ( ! $wc_dir ) {\n\t// Check if WooCommerce exists in the core plugin folder.\n\t$wc_dir = path_join( $wp_plugins_dir, '/woocommerce' );\n\tif ( ! file_exists( \"{$wc_dir}/woocommerce.php\" ) ) {\n\t\t// Check if WooCommerce exists in parent directory of the plugin.\n\t\t$wc_dir = path_join( dirname( $plugin_dir ), '/woocommerce' );\n\t}\n}\nvalidate_file_exists( \"{$wc_dir}/woocommerce.php\" );\n\n// Require the composer autoloader.\nrequire_once dirname( __DIR__ ) . '/vendor/autoload.php';\n\n// Give access to tests_add_filter() function.\nrequire_once \"{$wp_tests_dir}/includes/functions.php\";\n\ntests_add_filter(\n\t'muplugins_loaded',\n\tfunction () {\n\t\tload_plugins();\n\t}\n);\n\ntests_add_filter(\n\t'init',\n\tfunction () {\n\t\tinstall_woocommerce();\n\t},\n\t0\n);\n\n// Start up the WP testing environment.\nrequire \"{$wp_tests_dir}/includes/bootstrap.php\";\n\n// Start up the WC testing environment.\n$wc_tests_dir = $wc_dir . '/tests';\nif ( file_exists( $wc_dir . '/tests/legacy/bootstrap.php' ) ) {\n\t$wc_tests_dir .= '/legacy';\n}\n\n// Load WooCommerce test helpers.\n$wc_helper_files = array(\n\t'/framework/helpers/class-wc-helper-product.php',\n\t'/framework/helpers/class-wc-helper-shipping.php',\n\t'/framework/helpers/class-wc-helper-customer.php',\n\t'/framework/helpers/class-wc-helper-order.php',\n\t'/framework/helpers/class-wc-helper-coupon.php',\n);\n\nforeach ( $wc_helper_files as $helper_file ) {\n\tif ( file_exists( $wc_tests_dir . $helper_file ) ) {\n\t\trequire_once $wc_tests_dir . $helper_file;\n\t}\n}\n\n/**\n * Load WooCommerce for testing.\n *\n * @global $wc_dir\n */\nfunction install_woocommerce() {\n\tglobal $wc_dir;\n\n\tdefine( 'WP_UNINSTALL_PLUGIN', true );\n\tdefine( 'WC_REMOVE_ALL_DATA', true );\n\n\tinclude $wc_dir . '/uninstall.php';\n\n\tWC_Install::install();\n\n\t// Initialize the WC Admin extension if available.\n\tif ( class_exists( '\\Automattic\\WooCommerce\\Internal\\Admin\\Install' ) ) {\n\t\t\\Automattic\\WooCommerce\\Internal\\Admin\\Install::create_tables();\n\t\t\\Automattic\\WooCommerce\\Internal\\Admin\\Install::create_events();\n\t} elseif ( class_exists( '\\Automattic\\WooCommerce\\Admin\\Install' ) ) {\n\t\t\\Automattic\\WooCommerce\\Admin\\Install::create_tables();\n\t\t\\Automattic\\WooCommerce\\Admin\\Install::create_events();\n\t}\n\n\t// Reload capabilities after install.\n\t$GLOBALS['wp_roles'] = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited\n\twp_roles();\n\n\techo 'Installing WooCommerce...' . PHP_EOL;\n}\n\n/**\n * Manually load plugins.\n *\n * @global $plugin_dir\n * @global $wc_dir\n */\nfunction load_plugins() {\n\tglobal $plugin_dir;\n\tglobal $wc_dir;\n\n\trequire_once $wc_dir . '/woocommerce.php';\n\tupdate_option( 'woocommerce_db_version', WC()->version );\n\n\trequire $plugin_dir . '/wc-smooth-generator.php';\n}\n\n/**\n * Checks whether a file exists and throws an error if it doesn't.\n *\n * @param string $file_name The file path to check.\n */\nfunction validate_file_exists( string $file_name ) {\n\tif ( ! file_exists( $file_name ) ) {\n\t\techo \"Could not find {$file_name}, have you run bin/install-wp-tests.sh ?\" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\texit( 1 );\n\t}\n}\n\n/**\n * Join two paths together.\n *\n * @param string $base The base path.\n * @param string $path The path to append.\n *\n * @return string\n */\nfunction path_join( string $base, string $path ) {\n\treturn rtrim( $base, '/\\\\' ) . '/' . ltrim( $path, '/\\\\' );\n}\n"
  },
  {
    "path": "wc-smooth-generator.php",
    "content": "<?php\n/**\n * Plugin Name: WooCommerce Smooth Generator\n * Plugin URI: https://woocommerce.com\n * Description: A smooth product, order, customer, and coupon generator for WooCommerce.\n * Version: 1.3.0\n * Author: Automattic\n * Author URI: https://woocommerce.com\n *\n * Tested up to: 6.9\n * Requires PHP: 7.4\n * Requires Plugins: woocommerce\n * WC requires at least: 5.0.0\n * WC tested up to: 10.5\n * Woo: 000000:0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0\n *\n * @package WooCommerce\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// autoloader.\nif ( ! class_exists( \\WC\\SmoothGenerator\\Plugin::class ) ) {\n\trequire __DIR__ . '/vendor/autoload.php';\n}\n\n/**\n * Fetch instance of plugin.\n *\n * @return \\WC\\SmoothGenerator\\Plugin\n */\nfunction wc_smooth_generator() {\n\tstatic $instance;\n\n\tif ( is_null( $instance ) ) {\n\t\t$instance = new \\WC\\SmoothGenerator\\Plugin( __FILE__ );\n\t}\n\n\treturn $instance;\n}\n\n/**\n * Init plugin when WordPress loads.\n */\nfunction load_wc_smooth_generator() {\n\twc_smooth_generator();\n}\n\nif ( version_compare( PHP_VERSION, '7.4', '>=' ) ) {\n\tadd_action( 'plugins_loaded', 'load_wc_smooth_generator', 20 );\n}\n\n/**\n * Declare HPOS compatibility.\n */\nadd_action( 'before_woocommerce_init', function() {\n\tif ( class_exists( \\Automattic\\WooCommerce\\Utilities\\FeaturesUtil::class ) ) {\n\t\t\\Automattic\\WooCommerce\\Utilities\\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );\n\t}\n} );\n\n/**\n * Show action links on the plugin screen.\n *\n * @param array $links Plugin Action links.\n *\n * @return array\n */\nfunction wc_smooth_generator_plugin_action_links( $links ) {\n\t$action_links = array(\n\t\t'settings' => '<a href=\"' . esc_url( admin_url( 'tools.php?page=smoothgenerator' ) ) . '\" aria-label=\"' . esc_attr__( 'View WooCommerce Smooth Generator settings', 'wc-smooth-generator' ) . '\">' . esc_html__( 'Settings', 'wc-smooth-generator' ) . '</a>',\n\t);\n\n\treturn array_merge( $action_links, $links );\n}\n\nadd_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'wc_smooth_generator_plugin_action_links' );\n"
  }
]