Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7d6a57f01 | ||
|
|
ea2261d8d7 | ||
|
|
dd5965ae4c | ||
|
|
9bc7a62f20 | ||
|
|
ed66c96d3d | ||
|
|
ea64dbfb33 | ||
|
|
6507f4d8bb | ||
|
|
29a68b0be4 | ||
|
|
fb8ddf903e | ||
|
|
33d2836de0 | ||
|
|
c036a37602 | ||
|
|
efedd1bf29 | ||
|
|
12388af5a0 | ||
|
|
c39c13ffed | ||
|
|
7931dbeef9 | ||
|
|
ee81de86c2 | ||
|
|
669888817b | ||
|
|
5564b888fc | ||
|
|
91aca25169 | ||
|
|
4195fb2651 | ||
|
|
ba28ae174f | ||
|
|
755108a7d3 | ||
|
|
85983d5473 | ||
|
|
252b187600 | ||
|
|
8185a77697 | ||
|
|
6c2e317230 |
No files matched your search
@@ -0,0 +1,250 @@
|
||||
name: Create Release Package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: PHP Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, zip
|
||||
tools: composer:v2
|
||||
|
||||
- name: PHP Syntax Check
|
||||
run: |
|
||||
find includes -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||
find tests -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: |
|
||||
composer config platform.php 8.3.0
|
||||
composer install --optimize-autoloader --no-interaction
|
||||
|
||||
- name: Run PHPUnit tests
|
||||
run: vendor/bin/phpunit --testdox
|
||||
|
||||
build-release:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, zip, intl, gettext
|
||||
tools: composer:v2
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Building version: $VERSION"
|
||||
|
||||
- name: Validate composer.json
|
||||
run: composer validate --strict
|
||||
|
||||
- name: Install Composer dependencies (production)
|
||||
run: |
|
||||
composer config platform.php 8.3.0
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
|
||||
- name: Install gettext
|
||||
run: apt-get update && apt-get install -y gettext
|
||||
|
||||
- name: Compile translations
|
||||
run: |
|
||||
for po in languages/*.po; do
|
||||
if [ -f "$po" ]; then
|
||||
mo="${po%.po}.mo"
|
||||
echo "Compiling $po to $mo"
|
||||
msgfmt -o "$mo" "$po"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Verify plugin version matches tag
|
||||
run: |
|
||||
PLUGIN_VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" wc-composable-product.php | head -1)
|
||||
TAG_VERSION=${{ steps.version.outputs.version }}
|
||||
if [ "$PLUGIN_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "Error: Plugin version ($PLUGIN_VERSION) does not match tag version ($TAG_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Version verified: $PLUGIN_VERSION"
|
||||
|
||||
- name: Create release directory
|
||||
run: mkdir -p releases
|
||||
|
||||
- name: Build release package
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
PLUGIN_NAME="wc-composable-product"
|
||||
RELEASE_FILE="releases/${PLUGIN_NAME}-${VERSION}.zip"
|
||||
|
||||
# Move to parent directory for proper zip structure
|
||||
cd ..
|
||||
|
||||
# Create zip with proper WordPress plugin structure
|
||||
zip -r "${PLUGIN_NAME}/${RELEASE_FILE}" "${PLUGIN_NAME}" \
|
||||
-x "${PLUGIN_NAME}/.git/*" \
|
||||
-x "${PLUGIN_NAME}/.gitea/*" \
|
||||
-x "${PLUGIN_NAME}/.github/*" \
|
||||
-x "${PLUGIN_NAME}/.vscode/*" \
|
||||
-x "${PLUGIN_NAME}/.claude/*" \
|
||||
-x "${PLUGIN_NAME}/CLAUDE.md" \
|
||||
-x "${PLUGIN_NAME}/wp-core" \
|
||||
-x "${PLUGIN_NAME}/wp-core/*" \
|
||||
-x "${PLUGIN_NAME}/wp-plugins" \
|
||||
-x "${PLUGIN_NAME}/wp-plugins/*" \
|
||||
-x "${PLUGIN_NAME}/releases/*" \
|
||||
-x "${PLUGIN_NAME}/cache/*" \
|
||||
-x "${PLUGIN_NAME}/composer.lock" \
|
||||
-x "${PLUGIN_NAME}/*.log" \
|
||||
-x "${PLUGIN_NAME}/.gitignore" \
|
||||
-x "${PLUGIN_NAME}/.editorconfig" \
|
||||
-x "${PLUGIN_NAME}/phpcs.xml*" \
|
||||
-x "${PLUGIN_NAME}/phpunit.xml*" \
|
||||
-x "${PLUGIN_NAME}/tests/*" \
|
||||
-x "${PLUGIN_NAME}/*.po~" \
|
||||
-x "${PLUGIN_NAME}/*.bak" \
|
||||
-x "*.DS_Store"
|
||||
|
||||
cd "${PLUGIN_NAME}"
|
||||
echo "Created: ${RELEASE_FILE}"
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
|
||||
cd releases
|
||||
sha256sum "wc-composable-product-${VERSION}.zip" > "wc-composable-product-${VERSION}.zip.sha256"
|
||||
|
||||
echo "SHA256:"
|
||||
cat "wc-composable-product-${VERSION}.zip.sha256"
|
||||
|
||||
- name: Verify package structure
|
||||
run: |
|
||||
set +o pipefail
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
echo "Package contents:"
|
||||
unzip -l "releases/wc-composable-product-${VERSION}.zip" | head -50 || true
|
||||
|
||||
# Verify main file is at correct location
|
||||
if unzip -l "releases/wc-composable-product-${VERSION}.zip" | grep -q "wc-composable-product/wc-composable-product.php"; then
|
||||
echo "✓ Main plugin file at correct location"
|
||||
else
|
||||
echo "✗ Error: Main plugin file not found at wc-composable-product/wc-composable-product.php"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify vendor directory is included
|
||||
if unzip -l "releases/wc-composable-product-${VERSION}.zip" | grep -q "wc-composable-product/vendor/"; then
|
||||
echo "✓ Vendor directory included"
|
||||
else
|
||||
echo "✗ Error: Vendor directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Extract changelog for release notes
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
# Extract changelog section for this version
|
||||
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2)
|
||||
if [ -z "$NOTES" ]; then
|
||||
NOTES="Release version ${VERSION}"
|
||||
fi
|
||||
# Save to file for multi-line output
|
||||
echo "$NOTES" > release_notes.txt
|
||||
echo "Release notes extracted"
|
||||
|
||||
- name: Create Gitea Release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.SRC_GITEA_TOKEN }}
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
TAG_NAME=${{ github.ref_name }}
|
||||
PRERELEASE="false"
|
||||
if [[ "$TAG_NAME" == *-* ]]; then
|
||||
PRERELEASE="true"
|
||||
fi
|
||||
|
||||
# Read release notes
|
||||
BODY=$(cat release_notes.txt)
|
||||
|
||||
# Check if release already exists for this tag and delete it
|
||||
EXISTING_RELEASE=$(curl -s \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG_NAME}")
|
||||
|
||||
EXISTING_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty')
|
||||
|
||||
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
|
||||
echo "Deleting existing release ID: $EXISTING_ID"
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${EXISTING_ID}"
|
||||
fi
|
||||
|
||||
# Create release via Gitea API
|
||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${VERSION}\", \"body\": $(echo "$BODY" | jq -Rs .), \"draft\": false, \"prerelease\": ${PRERELEASE}}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases")
|
||||
|
||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||||
|
||||
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||
echo "Failed to create release:"
|
||||
echo "$RELEASE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Created release ID: $RELEASE_ID"
|
||||
|
||||
# Upload attachments
|
||||
for file in "releases/wc-composable-product-${VERSION}.zip" "releases/wc-composable-product-${VERSION}.zip.sha256"; do
|
||||
if [ -f "$file" ]; then
|
||||
FILENAME=$(basename "$file")
|
||||
echo "Uploading $FILENAME..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$file" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
|
||||
echo "Uploaded $FILENAME"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Release created successfully: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"
|
||||
Regular → Executable
+9
-6
@@ -1,14 +1,8 @@
|
||||
# Linked sources
|
||||
wp-core
|
||||
wp-plugins
|
||||
tpp
|
||||
|
||||
# Editor swap files
|
||||
*.*swp
|
||||
|
||||
# Composer
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# Cache
|
||||
cache/
|
||||
@@ -16,9 +10,18 @@ cache/
|
||||
# Development files
|
||||
.vscode/
|
||||
.idea/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.directory
|
||||
|
||||
# PHPUnit local overrides
|
||||
phpunit.xml
|
||||
|
||||
# Binary files
|
||||
languages/*.mo
|
||||
|
||||
.phpunit.result.cache
|
||||
+200
@@ -5,6 +5,206 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.3.1] - 2026-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- **PHPUnit test suite**: 57 unit tests covering all 6 core classes (ProductType, StockManager, CartHandler, Plugin, Admin/ProductData, Admin/Settings)
|
||||
- **Brain Monkey + Mockery** for WordPress/WooCommerce function mocking without a full WP installation
|
||||
- **WooCommerce class stubs** in `tests/stubs/` for classes extended by the plugin (WC_Product, WC_Settings_Page, etc.)
|
||||
- **PHP lint job** in release workflow — syntax-checks all PHP files before testing
|
||||
- **Test job** in release workflow — tests must pass before release package is built (`needs: test`)
|
||||
- Testing section in README
|
||||
|
||||
### Changed
|
||||
|
||||
- **PSR-4 refactored**: Renamed files to PascalCase (Product_Type → ProductType, etc.) and changed namespace from `WC_Composable_Product` to `Magdev\WcComposableProduct`
|
||||
- Updated all cross-references in PHP files, main plugin file, composer.json, CSS/JS doc comments, and translation file source comments
|
||||
- Release workflow now has three stages: lint → test → build-release
|
||||
|
||||
## [1.3.0] - 2026-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- **Include Non-Public Products**: New option to include draft and private products in composable product selections
|
||||
- Global setting under WooCommerce > Settings > Composable Products
|
||||
- Per-product override in the Composable Options tab (Use global default / Yes / No)
|
||||
- Useful when products should only be sold as part of a composition, not individually
|
||||
- Translations for the new setting in all 6 locales (de_DE, de_CH, fr_CH, it_CH + informal variants)
|
||||
|
||||
## [1.2.1] - 2026-03-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Consolidated documentation: merged INSTALL.md into README.md, merged IMPLEMENTATION.md into CLAUDE.md
|
||||
- Condensed CLAUDE.md from ~1960 lines to ~160 lines, keeping only essential architecture and lessons learned
|
||||
- README.md now includes full installation guide, usage tutorial, and troubleshooting section
|
||||
- Cleaned up .gitignore
|
||||
|
||||
### Removed
|
||||
|
||||
- INSTALL.md (content merged into README.md)
|
||||
- IMPLEMENTATION.md (content merged into CLAUDE.md)
|
||||
|
||||
## [1.2.0] - 2026-03-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: Product selection always empty regardless of configuration (categories, tags, or SKUs)
|
||||
- Root cause: Product query used a `meta_query` checking `_product_type` in `wp_postmeta`, but WooCommerce stores product types in the `product_type` **taxonomy** — the `!=` comparison with a non-existent meta key caused an INNER JOIN returning zero results
|
||||
- Fix: Replaced broken `meta_query` with correct `tax_query` using `product_type` taxonomy to exclude composable products
|
||||
- **CRITICAL**: Cart price always 0.00 despite correct frontend price calculation
|
||||
- Root cause: `composable_price_calculated` flag was persisted to the cart session, preventing price recalculation on subsequent page loads — but `set_price()` only modifies the in-memory product object and is lost between requests
|
||||
- Fix: Removed per-item session flag; the existing static `$already_calculated` flag already prevents duplicate calculation within a single request
|
||||
- **Admin tab rendering**: Both General and Composable Options panels visible on initial page load
|
||||
- Root cause: JavaScript manually showed `#composable_product_data` via `.show()` without hiding the General panel
|
||||
- Fix: Trigger WooCommerce's native tab click instead, so the tab system handles panel visibility correctly
|
||||
|
||||
### Added
|
||||
|
||||
- **Gitea CI/CD release workflow** (`.gitea/workflows/release.yml`)
|
||||
- Triggered on `v*` tags
|
||||
- Installs PHP 8.3 with production Composer dependencies
|
||||
- Compiles `.po` → `.mo` translations
|
||||
- Verifies plugin version matches tag
|
||||
- Builds release ZIP with proper WordPress directory structure
|
||||
- Generates SHA-256 checksums
|
||||
- Verifies package contains main plugin file and vendor directory
|
||||
- Extracts changelog for release notes
|
||||
- Creates Gitea release with attachments via API
|
||||
|
||||
### Removed
|
||||
|
||||
- Debug logging from v1.1.14 (no longer needed after root cause identified)
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified files: includes/Product_Type.php, includes/Cart_Handler.php, assets/js/admin.js
|
||||
- New file: .gitea/workflows/release.yml
|
||||
- Product query now correctly uses `tax_query` with `product_type` taxonomy (`NOT IN` operator)
|
||||
- Cart price recalculated on every request via `woocommerce_before_calculate_totals` hook
|
||||
- Admin JS uses `$('ul.product_data_tabs li.composable_options a').trigger('click')` for native WooCommerce tab handling
|
||||
|
||||
## [1.1.14] - 2025-12-31
|
||||
|
||||
### Added
|
||||
|
||||
- **DEBUG**: Comprehensive debug logging to troubleshoot product retrieval issues
|
||||
- Error log output shows selection criteria, query arguments, and results
|
||||
- Logs each product/variation being added to help identify filtering issues
|
||||
- Enable by setting WP_DEBUG to true in wp-config.php
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified file: includes/Product_Type.php (added error_log statements throughout get_available_products())
|
||||
- Logs criteria array (categories, tags, SKUs)
|
||||
- Logs WP_Query arguments before execution
|
||||
- Logs number of posts found by query
|
||||
- Logs each variable product's variation count
|
||||
- Logs each variation/simple product being added with name
|
||||
- Logs total products available at end
|
||||
- All logging wrapped in WP_DEBUG checks (no performance impact in production)
|
||||
|
||||
### Notes
|
||||
|
||||
- This is a debug release to help diagnose why products aren't showing
|
||||
- No functional changes from v1.1.13
|
||||
- User should enable WP_DEBUG and check debug.log or error.log
|
||||
- Log output will show exactly where products are being filtered out
|
||||
- All translation files remain at 100% completion (57/57 strings)
|
||||
|
||||
## [1.1.13] - 2025-12-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: "No products available for selection" message showing even when products are configured
|
||||
- Removed overly strict `is_in_stock()` requirement that was filtering out all products
|
||||
- Products now show regardless of stock status (out-of-stock items are displayed but disabled)
|
||||
- Added `'relation' => 'AND'` to meta_query for proper handling of multiple meta conditions
|
||||
|
||||
### Changed
|
||||
|
||||
- Product retrieval now shows all purchasable products, not just in-stock ones
|
||||
- Stock status still displayed on frontend with appropriate styling
|
||||
- Out-of-stock items shown but disabled via checkbox and visual indicators
|
||||
- Frontend stock management from v1.1.0 still fully functional
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified file: includes/Product_Type.php (lines 117-124, 177, 181)
|
||||
- Changed from `$variation->is_in_stock() && $variation->is_purchasable()` to just `$variation->is_purchasable()`
|
||||
- Changed from `$product->is_in_stock() && $product->is_purchasable()` to just `$product->is_purchasable()`
|
||||
- Added `'relation' => 'AND'` to meta_query array for WordPress query compatibility
|
||||
|
||||
### Notes
|
||||
|
||||
- This fixes the issue where NO products were showing in the selector
|
||||
- Stock validation still occurs at add-to-cart time (Stock_Manager class)
|
||||
- Frontend still displays stock badges (in stock, low stock, out of stock)
|
||||
- Out-of-stock items remain non-selectable via disabled checkboxes
|
||||
- All translation files remain at 100% completion (57/57 strings)
|
||||
|
||||
## [1.1.12] - 2025-12-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: Variable product variations still not appearing in product selector after v1.1.11 release
|
||||
- Changed variation retrieval method from `get_available_variations()` to `get_children()` for more reliable variation ID retrieval
|
||||
- `get_available_variations()` returns complex data arrays which may not work in all contexts
|
||||
- `get_children()` returns simple array of variation IDs directly, ensuring consistent results
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified file: includes/Product_Type.php (lines 171-184)
|
||||
- Changed from `$product->get_available_variations()` to `$product->get_children()`
|
||||
- More direct and reliable method for retrieving variation IDs
|
||||
- Each variation ID passed to `wc_get_product()` for full product object
|
||||
- Maintains all stock and purchasability checks from v1.1.11
|
||||
|
||||
### Notes
|
||||
|
||||
- This is a patch release fixing the variable product support introduced in v1.1.11
|
||||
- User reported "nope, still no product selectable" after v1.1.11
|
||||
- Root cause: `get_available_variations()` returns variation data arrays instead of clean IDs
|
||||
- `get_children()` is the standard WooCommerce method for retrieving variation IDs
|
||||
- All translation files remain at 100% completion (57/57 strings - no changes needed)
|
||||
|
||||
## [1.1.11] - 2025-12-31
|
||||
|
||||
### Added
|
||||
|
||||
- **FEATURE**: Variable product support - composable products can now include variable products and their variations
|
||||
- Variable products automatically expand to show all available variations as selectable items
|
||||
- Each variation displays with full attribute information (e.g., "Product - Color: Red, Size: Large")
|
||||
|
||||
### Fixed
|
||||
|
||||
- Products not showing in selector when all available products were variable products
|
||||
- Variable products were being filtered out because parent products aren't directly purchasable
|
||||
|
||||
### Changed
|
||||
|
||||
- Modified `get_available_products()` to detect and handle variable products
|
||||
- Variable products now expand into their individual variations
|
||||
- Each variation checked individually for stock status and purchasability
|
||||
- Simple products continue to work exactly as before
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified file: includes/Product_Type.php (lines 160-188)
|
||||
- Added logic to detect `is_type('variable')` products
|
||||
- Uses `get_available_variations()` to retrieve all variations
|
||||
- Each variation validated with `is_in_stock()` and `is_purchasable()`
|
||||
- Maintains backward compatibility with simple products
|
||||
|
||||
### Notes
|
||||
|
||||
- This is a feature enhancement release, not a bug fix
|
||||
- Resolves the issue where categories containing only variable products showed no selections
|
||||
- Variations display with their parent product name plus selected attributes
|
||||
- Stock management works correctly for both simple products and variations
|
||||
- All translation files remain at 100% completion (57/57 strings - no new strings added)
|
||||
|
||||
## [1.1.10] - 2025-12-31
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
# WooCommerce Composable Products - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a technical overview of the WooCommerce Composable Products plugin implementation.
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Created:** 2024-12-31
|
||||
**AI-Generated:** 100% created with Claude.AI assistance
|
||||
|
||||
## Architecture
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```txt
|
||||
wc-composable-product/
|
||||
├── assets/ # Frontend assets
|
||||
│ ├── css/
|
||||
│ │ ├── admin.css # Admin styles
|
||||
│ │ └── frontend.css # Frontend styles
|
||||
│ └── js/
|
||||
│ ├── admin.js # Admin JavaScript
|
||||
│ └── frontend.js # Frontend JavaScript
|
||||
├── cache/ # Twig template cache
|
||||
├── includes/ # PHP classes
|
||||
│ ├── Admin/
|
||||
│ │ ├── Product_Data.php # Product data tab
|
||||
│ │ └── Settings.php # Settings page
|
||||
│ ├── Cart_Handler.php # Cart integration
|
||||
│ ├── Plugin.php # Main plugin class
|
||||
│ ├── Product_Selector.php # Frontend selector
|
||||
│ └── Product_Type.php # Custom product type
|
||||
├── languages/ # Translation files
|
||||
│ └── wc-composable-product.pot
|
||||
├── templates/ # Twig templates
|
||||
│ └── product-selector.twig
|
||||
└── wc-composable-product.php # Main plugin file
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Main Plugin Class (`Plugin.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Singleton pattern implementation
|
||||
- Twig template engine initialization
|
||||
- Hook registration
|
||||
- Component initialization
|
||||
- Asset enqueuing
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
- `instance()`: Get singleton instance
|
||||
- `init_twig()`: Initialize Twig with WordPress functions
|
||||
- `render_template()`: Render Twig templates
|
||||
- `add_product_type()`: Register composable product type
|
||||
|
||||
### 2. Product Type (`Product_Type.php`)
|
||||
|
||||
**Extends:** `WC_Product`
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Custom product type: `composable`
|
||||
- Selection limit management (per-product or global)
|
||||
- Pricing mode (fixed or sum)
|
||||
- Product selection criteria (category/tag/SKU)
|
||||
- Dynamic product availability
|
||||
- Price calculation
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
- `get_selection_limit()`: Get max selectable items
|
||||
- `get_pricing_mode()`: Get pricing calculation mode
|
||||
- `get_available_products()`: Query available products
|
||||
- `calculate_composed_price()`: Calculate final price
|
||||
|
||||
### 3. Admin Settings (`Admin/Settings.php`)
|
||||
|
||||
**Extends:** `WC_Settings_Page`
|
||||
|
||||
**Global Settings:**
|
||||
|
||||
- Default selection limit
|
||||
- Default pricing mode
|
||||
- Display options (images, prices, total)
|
||||
|
||||
**Integration:** Adds tab to WooCommerce Settings
|
||||
|
||||
### 4. Product Data Tab (`Admin/Product_Data.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Add "Composable Options" tab to product edit page
|
||||
- Render selection criteria fields
|
||||
- Save product meta data
|
||||
- Dynamic field visibility based on criteria type
|
||||
|
||||
**Saved Meta:**
|
||||
|
||||
- `_composable_selection_limit`: Item limit
|
||||
- `_composable_pricing_mode`: Pricing calculation
|
||||
- `_composable_criteria_type`: Selection method
|
||||
- `_composable_categories`: Selected categories
|
||||
- `_composable_tags`: Selected tags
|
||||
- `_composable_skus`: SKU list
|
||||
|
||||
### 5. Product Selector (`Product_Selector.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Render frontend product selection interface
|
||||
- Prepare data for Twig template
|
||||
- Apply display settings
|
||||
|
||||
**Template Variables:**
|
||||
|
||||
- `products`: Available products array
|
||||
- `selection_limit`: Max selections
|
||||
- `pricing_mode`: Pricing calculation
|
||||
- `show_images/prices/total`: Display flags
|
||||
|
||||
### 6. Cart Handler (`Cart_Handler.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Validate product selection
|
||||
- Add selected products to cart data
|
||||
- Calculate dynamic pricing
|
||||
- Display selected products in cart
|
||||
|
||||
**Hooks:**
|
||||
|
||||
- `woocommerce_add_to_cart_validation`: Validate selections
|
||||
- `woocommerce_add_cart_item_data`: Store selections
|
||||
- `woocommerce_before_calculate_totals`: Update prices
|
||||
- `woocommerce_get_item_data`: Display in cart
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Product Selector Template (`product-selector.twig`)
|
||||
|
||||
**Features:**
|
||||
|
||||
- Responsive grid layout
|
||||
- Checkbox-based selection
|
||||
- Product images and prices
|
||||
- Real-time total calculation
|
||||
- AJAX add-to-cart
|
||||
|
||||
**Data Attributes:**
|
||||
|
||||
- `data-product-id`: Composable product ID
|
||||
- `data-selection-limit`: Max selections
|
||||
- `data-pricing-mode`: Pricing mode
|
||||
- `data-price`: Individual product prices
|
||||
|
||||
### JavaScript (`frontend.js`)
|
||||
|
||||
**Functionality:**
|
||||
|
||||
- Selection limit enforcement
|
||||
- Visual feedback on selection
|
||||
- Real-time price updates (sum mode)
|
||||
- AJAX cart operations
|
||||
- Error/success messages
|
||||
|
||||
**Key Functions:**
|
||||
|
||||
- `handleCheckboxChange()`: Selection logic
|
||||
- `updateTotalPrice()`: Calculate total
|
||||
- `addToCart()`: AJAX add-to-cart
|
||||
- `showMessage()`: User feedback
|
||||
|
||||
### CSS Styling
|
||||
|
||||
**Approach:**
|
||||
|
||||
- Grid-based layout (responsive)
|
||||
- Card-style product items
|
||||
- Visual selection states
|
||||
- Mobile-first design
|
||||
- Breakpoints: 768px, 480px
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Creating a Composable Product
|
||||
|
||||
1. Admin selects "Composable product" type
|
||||
2. Configure selection limit and pricing mode
|
||||
3. Choose selection criteria (category/tag/SKU)
|
||||
4. Save product metadata
|
||||
5. WooCommerce registers product with custom type
|
||||
|
||||
### Frontend Display
|
||||
|
||||
1. Customer visits product page
|
||||
2. `Cart_Handler` renders `Product_Selector`
|
||||
3. `Product_Type::get_available_products()` queries products
|
||||
4. Twig template renders grid with products
|
||||
5. JavaScript handles interactions
|
||||
|
||||
### Adding to Cart
|
||||
|
||||
1. Customer selects products (JavaScript validation)
|
||||
2. Click "Add to Cart" button
|
||||
3. AJAX request with selected product IDs
|
||||
4. `Cart_Handler::validate_add_to_cart()` validates
|
||||
5. `Cart_Handler::add_cart_item_data()` stores selections
|
||||
6. `Cart_Handler::calculate_cart_item_price()` updates price
|
||||
7. Product added to cart with custom data
|
||||
|
||||
### Cart Display
|
||||
|
||||
1. WooCommerce loads cart
|
||||
2. `Cart_Handler::get_cart_item_from_session()` restores data
|
||||
3. `Cart_Handler::display_cart_item_data()` shows selections
|
||||
4. Price calculated dynamically on each cart load
|
||||
|
||||
## Security Implementation
|
||||
|
||||
### Input Sanitization
|
||||
|
||||
- **Integers:** `absint()` for IDs and limits
|
||||
- **Text:** `sanitize_text_field()` for modes and types
|
||||
- **Textarea:** `sanitize_textarea_field()` for SKUs
|
||||
- **Arrays:** `array_map()` with sanitization functions
|
||||
|
||||
### Output Escaping
|
||||
|
||||
- **HTML:** `esc_html()`, `esc_html_e()`
|
||||
- **Attributes:** `esc_attr()`
|
||||
- **URLs:** `esc_url()`
|
||||
- **JavaScript:** Localized scripts with escaped data
|
||||
|
||||
### Validation
|
||||
|
||||
- Selection limit enforcement
|
||||
- Product availability verification
|
||||
- Cart data validation
|
||||
- Nonce verification (via WooCommerce)
|
||||
|
||||
## Internationalization
|
||||
|
||||
### Text Domain
|
||||
|
||||
`wc-composable-product`
|
||||
|
||||
### Translation Functions
|
||||
|
||||
- `__()`: Return translated string
|
||||
- `_e()`: Echo translated string
|
||||
- `sprintf()` with `__()`: Variable substitution
|
||||
|
||||
### POT File
|
||||
|
||||
Generated template: `languages/wc-composable-product.pot`
|
||||
|
||||
**Supported Locales (per CLAUDE.md):**
|
||||
|
||||
- en_US (English)
|
||||
- de_DE, de_DE_informal (German - Germany)
|
||||
- de_CH, de_CH_informal (German - Switzerland)
|
||||
- fr_CH (French - Switzerland)
|
||||
- it_CH (Italian - Switzerland)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching
|
||||
|
||||
- Twig templates cached in `cache/` directory
|
||||
- Auto-reload enabled in debug mode
|
||||
- Optimized Composer autoloader
|
||||
|
||||
### Database Queries
|
||||
|
||||
- Efficient `WP_Query` for product selection
|
||||
- Meta queries for SKU filtering
|
||||
- Taxonomy queries for category/tag filtering
|
||||
|
||||
### Asset Loading
|
||||
|
||||
- Scripts only on relevant pages
|
||||
- Minification ready (use build tools)
|
||||
- Conditional enqueuing
|
||||
|
||||
## Extensibility
|
||||
|
||||
### Hooks & Filters
|
||||
|
||||
**Available Filters:**
|
||||
|
||||
- `wc_composable_settings`: Modify settings array
|
||||
- `woocommerce_product_class`: Custom product class
|
||||
- `product_type_selector`: Product type registration
|
||||
|
||||
**Customization Points:**
|
||||
|
||||
- Twig templates (override in theme)
|
||||
- CSS styling (enqueue custom styles)
|
||||
- JavaScript behavior (extend object)
|
||||
|
||||
### Developer API
|
||||
|
||||
```php
|
||||
// Get composable product
|
||||
$product = wc_get_product($product_id);
|
||||
|
||||
// Check if composable
|
||||
if ($product->get_type() === 'composable') {
|
||||
// Get available products
|
||||
$products = $product->get_available_products();
|
||||
|
||||
// Get selection limit
|
||||
$limit = $product->get_selection_limit();
|
||||
|
||||
// Calculate price
|
||||
$price = $product->calculate_composed_price($selected_ids);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Admin Testing
|
||||
|
||||
- [ ] Product type appears in dropdown
|
||||
- [ ] Composable Options tab displays
|
||||
- [ ] Selection criteria toggle works
|
||||
- [ ] Meta data saves correctly
|
||||
- [ ] Settings page accessible
|
||||
- [ ] Global defaults apply
|
||||
|
||||
### Frontend Testing
|
||||
|
||||
- [ ] Product selector renders
|
||||
- [ ] Selection limit enforced
|
||||
- [ ] Price calculation accurate (both modes)
|
||||
- [ ] AJAX add-to-cart works
|
||||
- [ ] Cart displays selections
|
||||
- [ ] Checkout processes correctly
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- [ ] Empty criteria (no products)
|
||||
- [ ] Out of stock products excluded
|
||||
- [ ] Invalid product selections rejected
|
||||
- [ ] Multiple cart items unique
|
||||
- [ ] Session persistence
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Variable Products:** Currently supports simple products in selection
|
||||
2. **Grouped Products:** Cannot be used as selectable items
|
||||
3. **Stock Management:** No automatic stock reduction for selected items
|
||||
4. **Caching:** Template cache needs manual clearing after updates
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential features for future versions:
|
||||
|
||||
- Variable product support in selection
|
||||
- Quantity selection per item (not just presence)
|
||||
- Visual bundle previews
|
||||
- Advanced pricing rules
|
||||
- Stock management integration
|
||||
- Product recommendations
|
||||
- Selection templates/presets
|
||||
- Multi-currency support enhancements
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime
|
||||
|
||||
- PHP 8.3+
|
||||
- WordPress 6.0+
|
||||
- WooCommerce 8.0+
|
||||
- Twig 3.0 (via Composer)
|
||||
|
||||
### Development
|
||||
|
||||
- Composer for dependency management
|
||||
- WP-CLI for i18n operations (optional)
|
||||
|
||||
## Deployment
|
||||
|
||||
### Production Checklist
|
||||
|
||||
1. Run `composer install --no-dev --optimize-autoloader`
|
||||
2. Ensure `vendor/` directory is included
|
||||
3. Ensure `cache/` directory is writable
|
||||
4. Test on staging environment
|
||||
5. Clear all caches after activation
|
||||
6. Verify WooCommerce compatibility
|
||||
|
||||
### Release Package
|
||||
|
||||
Must include:
|
||||
|
||||
- All PHP files
|
||||
- `vendor/` directory
|
||||
- Assets (CSS, JS)
|
||||
- Templates
|
||||
- Language files
|
||||
- Documentation
|
||||
|
||||
Must exclude:
|
||||
|
||||
- `.git/` directory
|
||||
- `composer.lock`
|
||||
- Development files
|
||||
- `wp-core/`, `wp-plugins/` symlinks
|
||||
|
||||
## Support & Maintenance
|
||||
|
||||
### Code Standards
|
||||
|
||||
- WordPress Coding Standards
|
||||
- WooCommerce best practices
|
||||
- PSR-4 autoloading
|
||||
- Inline documentation
|
||||
|
||||
### Version Control
|
||||
|
||||
- Semantic versioning (MAJOR.MINOR.PATCH)
|
||||
- Changelog maintained
|
||||
- Annotated git tags
|
||||
- Development on `dev` branch
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2024-12-31
|
||||
**Maintainer:** Marco Graetsch
|
||||
**AI Assistant:** Claude.AI (Anthropic)
|
||||
-150
@@ -1,150 +0,0 @@
|
||||
# Installation Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
Before installing the WooCommerce Composable Products plugin, ensure your system meets these requirements:
|
||||
|
||||
- **PHP**: 8.3 or higher
|
||||
- **WordPress**: 6.0 or higher
|
||||
- **WooCommerce**: 8.0 or higher
|
||||
- **Composer**: For dependency management
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Upload Plugin Files
|
||||
|
||||
Upload the plugin directory to your WordPress installation:
|
||||
|
||||
```bash
|
||||
/wp-content/plugins/wc-composable-product/
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
Navigate to the plugin directory and install dependencies:
|
||||
|
||||
```bash
|
||||
cd /wp-content/plugins/wc-composable-product/
|
||||
composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
|
||||
### 3. Activate Plugin
|
||||
|
||||
1. Log in to your WordPress admin panel
|
||||
2. Navigate to **Plugins > Installed Plugins**
|
||||
3. Find "WooCommerce Composable Products"
|
||||
4. Click **Activate**
|
||||
|
||||
### 4. Configure Settings
|
||||
|
||||
After activation, configure the plugin:
|
||||
|
||||
1. Navigate to **WooCommerce > Settings**
|
||||
2. Click on the **Composable Products** tab
|
||||
3. Configure default settings:
|
||||
- **Default Selection Limit**: Number of items customers can select (default: 5)
|
||||
- **Default Pricing Mode**: Choose between "Sum of selected products" or "Fixed price"
|
||||
- **Display Options**: Toggle product images, prices, and totals
|
||||
|
||||
## Creating Your First Composable Product
|
||||
|
||||
### Step 1: Create a New Product
|
||||
|
||||
1. Go to **Products > Add New**
|
||||
2. Enter a product name (e.g., "Custom Sticker Pack")
|
||||
|
||||
### Step 2: Set Product Type
|
||||
|
||||
1. In the **Product Data** panel, select **Composable product** from the dropdown
|
||||
|
||||
### Step 3: Configure General Settings
|
||||
|
||||
In the **General** tab:
|
||||
- Set a **Regular price** (used if pricing mode is "Fixed")
|
||||
- Configure **Selection Limit** (leave empty to use global default)
|
||||
- Choose **Pricing Mode** (leave empty to use global default)
|
||||
|
||||
### Step 4: Configure Composable Options
|
||||
|
||||
Click on the **Composable Options** tab:
|
||||
|
||||
1. **Selection Criteria**: Choose how to select available products
|
||||
- **By Category**: Select product categories
|
||||
- **By Tag**: Select product tags
|
||||
- **By SKU**: Enter comma-separated SKUs
|
||||
|
||||
2. Based on your selection:
|
||||
- **Categories**: Select one or more categories from the dropdown
|
||||
- **Tags**: Select one or more tags from the dropdown
|
||||
- **SKUs**: Enter SKUs like: `STICKER-01, STICKER-02, STICKER-03`
|
||||
|
||||
### Step 5: Publish
|
||||
|
||||
Click **Publish** to make your composable product live.
|
||||
|
||||
## Frontend Usage
|
||||
|
||||
When customers visit your composable product:
|
||||
|
||||
1. They see a grid of available products based on your criteria
|
||||
2. They can select up to the configured limit
|
||||
3. The total price updates in real-time (if using sum pricing mode)
|
||||
4. Click "Add to Cart" to add the composition to their cart
|
||||
5. Selected products are displayed in the cart
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Won't Activate
|
||||
|
||||
- Ensure WooCommerce is installed and activated first
|
||||
- Check PHP version (must be 8.3+)
|
||||
- Verify Composer dependencies are installed
|
||||
|
||||
### Products Not Showing in Selector
|
||||
|
||||
- Check that products are published and in stock
|
||||
- Verify the selection criteria (category/tag/SKU) is correct
|
||||
- Ensure products match the criteria you configured
|
||||
|
||||
### Twig Template Errors
|
||||
|
||||
- Ensure the `vendor/` directory exists and contains Twig
|
||||
- Run `composer install` again
|
||||
- Check that the `cache/` directory is writable
|
||||
|
||||
### JavaScript Not Working
|
||||
|
||||
- Clear browser cache
|
||||
- Check browser console for errors
|
||||
- Ensure jQuery is loaded (WooCommerce includes it)
|
||||
|
||||
## Updating
|
||||
|
||||
When updating the plugin:
|
||||
|
||||
1. Deactivate the plugin
|
||||
2. Replace plugin files
|
||||
3. Run `composer install --no-dev --optimize-autoloader`
|
||||
4. Reactivate the plugin
|
||||
5. Clear all caches (WordPress, browser, CDN)
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To completely remove the plugin:
|
||||
|
||||
1. Deactivate the plugin
|
||||
2. Delete the plugin from the Plugins page
|
||||
3. Optionally clean up database entries (WooCommerce will handle this automatically)
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests:
|
||||
- GitHub: https://github.com/magdev/wc-composable-product/issues
|
||||
- Documentation: See README.md
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Customize the template by editing `templates/product-selector.twig`
|
||||
- Modify styles in `assets/css/frontend.css`
|
||||
- Translate the plugin using the provided `.pot` file
|
||||
- Create categories/tags for easier product organization
|
||||
@@ -1,51 +1,113 @@
|
||||
# WooCommerce Composable Products
|
||||
|
||||
Create composable products where customers can select a limited number of items from a configurable set of products.
|
||||
Create composable products where customers can select a limited number of items from a configurable set of products. Think of it as a "build your own gift box" or "create your sticker pack" feature.
|
||||
|
||||
## Description
|
||||
|
||||
This plugin adds a new product type to WooCommerce that allows customers to build their own product bundles by selecting from a predefined set of simple or variable products. Think of it as a "build your own gift box" or "create your sticker pack" feature.
|
||||
|
||||
### Key Features
|
||||
## Key Features
|
||||
|
||||
- **Custom Product Type**: New "Composable Product" type in WooCommerce
|
||||
- **Flexible Selection**: Define available products by category, tag, or SKU
|
||||
- **Variable Product Support**: Automatically expands variable products into selectable variations
|
||||
- **Stock Management**: Real-time stock validation, visual indicators, and automatic inventory tracking
|
||||
- **Configurable Limits**: Set global or per-product selection limits
|
||||
- **Pricing Options**: Fixed price or sum of selected products
|
||||
- **Multi-language Support**: Fully translatable with i18n support
|
||||
- **Pricing Options**: Fixed price or sum of selected products with full locale-aware formatting
|
||||
- **Multi-language Support**: Fully translated in 6 locales (de_DE, de_CH, fr_CH, it_CH + informal variants)
|
||||
- **Modern UI**: Clean interface built with Twig templates and vanilla JavaScript
|
||||
- **Tested**: 57 unit tests with PHPUnit, Brain Monkey, and Mockery
|
||||
- **CI/CD**: Automated release workflow with test gate for Gitea
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.3 or higher
|
||||
- WordPress 6.0 or higher
|
||||
- WooCommerce 8.0 or higher
|
||||
- Composer (for dependency management)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Upload the plugin files to `/wp-content/plugins/wc-composable-product/`
|
||||
2. Run `composer install --no-dev` in the plugin directory
|
||||
3. Activate the plugin through the 'Plugins' menu in WordPress
|
||||
4. Configure global settings under WooCommerce > Settings > Composable Products
|
||||
### From Release Package
|
||||
|
||||
1. Download the latest release ZIP from the releases page
|
||||
2. In WordPress admin, go to **Plugins > Add New > Upload Plugin**
|
||||
3. Upload the ZIP file and click **Install Now**
|
||||
4. Activate the plugin through the **Plugins** menu
|
||||
5. Configure global settings under **WooCommerce > Settings > Composable Products**
|
||||
|
||||
### From Source
|
||||
|
||||
1. Upload the plugin directory to `/wp-content/plugins/wc-composable-product/`
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
cd /wp-content/plugins/wc-composable-product/
|
||||
composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
|
||||
3. Activate the plugin through the **Plugins** menu in WordPress
|
||||
4. Configure global settings under **WooCommerce > Settings > Composable Products**
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Composable Product
|
||||
|
||||
1. Go to Products > Add New
|
||||
2. Select "Composable Product" as the product type
|
||||
3. Configure product details:
|
||||
- Set the selection limit (or use global default)
|
||||
- Choose pricing mode (fixed or sum)
|
||||
- Define available products by category, tag, or SKU
|
||||
4. Publish the product
|
||||
|
||||
### Global Settings
|
||||
|
||||
Navigate to WooCommerce > Settings > Composable Products to configure:
|
||||
- Default selection limit
|
||||
- Default pricing mode
|
||||
- Display options
|
||||
Navigate to **WooCommerce > Settings > Composable Products** to configure:
|
||||
|
||||
- **Default Selection Limit**: Number of items customers can select (default: 5)
|
||||
- **Default Pricing Mode**: Choose between "Sum of selected products" or "Fixed price"
|
||||
- **Display Options**: Toggle product images, prices, and totals
|
||||
|
||||
### Creating a Composable Product
|
||||
|
||||
1. Go to **Products > Add New**
|
||||
2. Select **Composable product** from the Product Data dropdown
|
||||
3. In the **General** tab:
|
||||
- Set a **Regular price** (used when pricing mode is "Fixed")
|
||||
- Configure **Selection Limit** (leave empty to use global default)
|
||||
- Choose **Pricing Mode** (leave empty to use global default)
|
||||
4. Click the **Composable Options** tab:
|
||||
- **Selection Criteria**: Choose how to define available products
|
||||
- **By Category**: Select one or more product categories
|
||||
- **By Tag**: Select one or more product tags
|
||||
- **By SKU**: Enter comma-separated SKUs (e.g., `STICKER-01, STICKER-02`)
|
||||
5. Click **Publish**
|
||||
|
||||
### Frontend Behavior
|
||||
|
||||
When customers visit a composable product page:
|
||||
|
||||
1. A grid of available products is displayed based on configured criteria
|
||||
2. Customers select up to the configured limit via checkboxes
|
||||
3. Total price updates in real-time (in sum pricing mode)
|
||||
4. Stock indicators show availability (green/orange/red badges)
|
||||
5. Click "Add to Cart" to add the composition to cart
|
||||
6. Selected products are listed in the cart and checkout
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Won't Activate
|
||||
|
||||
- Ensure WooCommerce is installed and activated first
|
||||
- Check PHP version (must be 8.3+)
|
||||
- Verify Composer dependencies are installed (`vendor/` directory exists)
|
||||
|
||||
### Products Not Showing in Selector
|
||||
|
||||
- Check that products are published
|
||||
- Verify the selection criteria (category/tag/SKU) matches existing products
|
||||
- Ensure the criteria type and values are saved in the Composable Options tab
|
||||
|
||||
### Twig Template Errors
|
||||
|
||||
- Ensure the `vendor/` directory exists and contains Twig
|
||||
- Run `composer install` again
|
||||
- Check that the `cache/` directory is writable
|
||||
|
||||
### Updating
|
||||
|
||||
1. Deactivate the plugin
|
||||
2. Replace plugin files (or upload new release ZIP)
|
||||
3. If installed from source: run `composer install --no-dev --optimize-autoloader`
|
||||
4. Reactivate the plugin
|
||||
5. Clear all caches (WordPress, browser, CDN)
|
||||
|
||||
## Development
|
||||
|
||||
@@ -57,13 +119,41 @@ This project was created with AI assistance (Claude.AI) and follows WordPress an
|
||||
composer install
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
The plugin includes a PHPUnit test suite with Brain Monkey for WordPress function mocking:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit --testdox
|
||||
```
|
||||
|
||||
Tests run without a WordPress installation. WooCommerce classes are provided as minimal stubs in `tests/stubs/`.
|
||||
|
||||
### Translation
|
||||
|
||||
Generate POT file:
|
||||
|
||||
```bash
|
||||
wp i18n make-pot . languages/wc-composable-product.pot
|
||||
```
|
||||
|
||||
Compile translations:
|
||||
|
||||
```bash
|
||||
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||
```
|
||||
|
||||
### Creating Releases
|
||||
|
||||
Releases are automated via Gitea CI/CD. Push an annotated tag to trigger:
|
||||
|
||||
```bash
|
||||
git tag -a v1.2.0 -m "Release v1.2.0"
|
||||
git push origin v1.2.0
|
||||
```
|
||||
|
||||
The workflow builds the release ZIP, compiles translations, generates checksums, and creates a Gitea release with attachments.
|
||||
|
||||
## License
|
||||
|
||||
GPL v3 or later - see LICENSE file for details
|
||||
@@ -74,4 +164,4 @@ Marco Graetsch
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests, please use the GitHub issue tracker.
|
||||
For issues and feature requests, please use the issue tracker.
|
||||
Regular → Executable
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Admin Styles for Composable Products
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
/* Hide composable panel by default */
|
||||
|
||||
Regular → Executable
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Frontend Styles for Composable Products
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
.wc-composable-product-selector {
|
||||
|
||||
Regular → Executable
+6
-4
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Admin JavaScript for Composable Products
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
@@ -17,12 +17,14 @@
|
||||
if (productType === 'composable') {
|
||||
$('.show_if_composable').show();
|
||||
$('.hide_if_composable').hide();
|
||||
$('#composable_product_data').show();
|
||||
$('.product_data_tabs .composable_options a').show();
|
||||
// Show the composable tab, then click it so WooCommerce's
|
||||
// native tab system hides all other panels properly
|
||||
$('.product_data_tabs li.composable_options').show();
|
||||
$('ul.product_data_tabs li.composable_options a').trigger('click');
|
||||
} else {
|
||||
$('.show_if_composable').hide();
|
||||
$('.product_data_tabs li.composable_options').hide();
|
||||
$('#composable_product_data').hide();
|
||||
$('.product_data_tabs .composable_options a').hide();
|
||||
}
|
||||
}).trigger('change');
|
||||
|
||||
|
||||
Regular → Executable
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Frontend JavaScript for Composable Products
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
|
||||
+12
-2
@@ -6,7 +6,7 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marco Graetsch",
|
||||
"email": "marco@example.com"
|
||||
"email": "magdev3.0@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
@@ -15,7 +15,17 @@
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WC_Composable_Product\\": "includes/"
|
||||
"Magdev\\WcComposableProduct\\": "includes/"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"brain/monkey": "^2.7",
|
||||
"mockery/mockery": "^1.6",
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Magdev\\WcComposableProduct\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
||||
Generated
+2384
File diff suppressed because it is too large.
Load diff
@@ -2,17 +2,17 @@
|
||||
/**
|
||||
* Product Data Tab
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product\Admin;
|
||||
namespace Magdev\WcComposableProduct\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Product Data Tab Class
|
||||
*/
|
||||
class Product_Data {
|
||||
class ProductData {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
@@ -95,6 +95,19 @@ class Product_Data {
|
||||
<div id="composable_product_data" class="panel woocommerce_options_panel hidden">
|
||||
<div class="options_group">
|
||||
<?php
|
||||
woocommerce_wp_select([
|
||||
'id' => '_composable_include_unpublished',
|
||||
'label' => __('Include Non-Public Products', 'wc-composable-product'),
|
||||
'description' => __('Allow draft and private products in the selection. Useful when products should only be sold as part of a composition.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'options' => [
|
||||
'' => __('Use global default', 'wc-composable-product'),
|
||||
'yes' => __('Yes', 'wc-composable-product'),
|
||||
'no' => __('No', 'wc-composable-product'),
|
||||
],
|
||||
'value' => get_post_meta($post->ID, '_composable_include_unpublished', true) ?: '',
|
||||
]);
|
||||
|
||||
woocommerce_wp_select([
|
||||
'id' => '_composable_criteria_type',
|
||||
'label' => __('Selection Criteria', 'wc-composable-product'),
|
||||
@@ -181,6 +194,10 @@ class Product_Data {
|
||||
$pricing_mode = isset($_POST['_composable_pricing_mode']) ? sanitize_text_field($_POST['_composable_pricing_mode']) : '';
|
||||
update_post_meta($post_id, '_composable_pricing_mode', $pricing_mode);
|
||||
|
||||
// Save include unpublished
|
||||
$include_unpublished = isset($_POST['_composable_include_unpublished']) ? sanitize_text_field($_POST['_composable_include_unpublished']) : '';
|
||||
update_post_meta($post_id, '_composable_include_unpublished', $include_unpublished);
|
||||
|
||||
// Save criteria type
|
||||
$criteria_type = isset($_POST['_composable_criteria_type']) ? sanitize_text_field($_POST['_composable_criteria_type']) : 'category';
|
||||
update_post_meta($post_id, '_composable_criteria_type', $criteria_type);
|
||||
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Admin Settings
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product\Admin;
|
||||
namespace Magdev\WcComposableProduct\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -60,6 +60,13 @@ class Settings extends \WC_Settings_Page {
|
||||
],
|
||||
'desc_tip' => true,
|
||||
],
|
||||
[
|
||||
'title' => __('Include Non-Public Products', 'wc-composable-product'),
|
||||
'desc' => __('Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_include_unpublished',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'no',
|
||||
],
|
||||
[
|
||||
'title' => __('Show Product Images', 'wc-composable-product'),
|
||||
'desc' => __('Display product images in the selection interface.', 'wc-composable-product'),
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Cart Handler
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -14,11 +14,11 @@ defined('ABSPATH') || exit;
|
||||
*
|
||||
* Handles adding composable products to cart and calculating prices
|
||||
*/
|
||||
class Cart_Handler {
|
||||
class CartHandler {
|
||||
/**
|
||||
* Stock manager instance
|
||||
*
|
||||
* @var Stock_Manager
|
||||
* @var StockManager
|
||||
*/
|
||||
private $stock_manager;
|
||||
|
||||
@@ -26,7 +26,7 @@ class Cart_Handler {
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->stock_manager = new Stock_Manager();
|
||||
$this->stock_manager = new StockManager();
|
||||
|
||||
add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3);
|
||||
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 2);
|
||||
@@ -59,7 +59,7 @@ class Cart_Handler {
|
||||
global $product;
|
||||
|
||||
if ($product && $product->get_type() === 'composable') {
|
||||
Product_Selector::render($product);
|
||||
ProductSelector::render($product);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ class Cart_Handler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use static flag to prevent multiple executions
|
||||
// Use static flag to prevent multiple executions within the same request
|
||||
static $already_calculated = false;
|
||||
if ($already_calculated) {
|
||||
return;
|
||||
@@ -208,13 +208,10 @@ class Cart_Handler {
|
||||
|
||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||
if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') {
|
||||
if (isset($cart_item['composable_products']) && !isset($cart_item['composable_price_calculated'])) {
|
||||
if (isset($cart_item['composable_products'])) {
|
||||
$product = $cart_item['data'];
|
||||
$price = $product->calculate_composed_price($cart_item['composable_products']);
|
||||
$cart_item['data']->set_price($price);
|
||||
|
||||
// Mark as calculated to prevent re-calculation by other plugins
|
||||
$cart->cart_contents[$cart_item_key]['composable_price_calculated'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Main Plugin Class
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -98,15 +98,15 @@ class Plugin {
|
||||
private function includes() {
|
||||
// Note: Settings.php is NOT included here because it extends WC_Settings_Page
|
||||
// which isn't loaded until later. It's included in add_settings_page() instead.
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Product_Data.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Type.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Stock_Manager.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Cart_Handler.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Selector.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/ProductData.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductType.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/StockManager.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/CartHandler.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductSelector.php';
|
||||
|
||||
// Initialize components
|
||||
new Admin\Product_Data();
|
||||
new Cart_Handler();
|
||||
new Admin\ProductData();
|
||||
new CartHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +129,7 @@ class Plugin {
|
||||
*/
|
||||
public function product_class($classname, $product_type) {
|
||||
if ($product_type === 'composable') {
|
||||
$classname = 'WC_Composable_Product\Product_Type';
|
||||
$classname = 'Magdev\WcComposableProduct\ProductType';
|
||||
}
|
||||
return $classname;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Product Selector
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -14,11 +14,11 @@ defined('ABSPATH') || exit;
|
||||
*
|
||||
* Handles rendering the product selection interface
|
||||
*/
|
||||
class Product_Selector {
|
||||
class ProductSelector {
|
||||
/**
|
||||
* Render product selector
|
||||
*
|
||||
* @param Product_Type $product Composable product
|
||||
* @param ProductType $product Composable product
|
||||
*/
|
||||
public static function render($product) {
|
||||
if (!$product || $product->get_type() !== 'composable') {
|
||||
@@ -34,7 +34,7 @@ class Product_Selector {
|
||||
$show_total = get_option('wc_composable_show_total', 'yes') === 'yes';
|
||||
|
||||
// Get stock manager for stock information
|
||||
$stock_manager = new Stock_Manager();
|
||||
$stock_manager = new StockManager();
|
||||
|
||||
// Prepare product data for template
|
||||
$products_data = [];
|
||||
@@ -2,17 +2,17 @@
|
||||
/**
|
||||
* Composable Product Type
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Composable Product Type Class
|
||||
*/
|
||||
class Product_Type extends \WC_Product {
|
||||
class ProductType extends \WC_Product {
|
||||
/**
|
||||
* Product type
|
||||
*
|
||||
@@ -97,6 +97,22 @@ class Product_Type extends \WC_Product {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if non-public products should be included
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_include_unpublished() {
|
||||
$per_product = $this->get_meta('_composable_include_unpublished', true);
|
||||
if ($per_product === 'yes') {
|
||||
return true;
|
||||
}
|
||||
if ($per_product === 'no') {
|
||||
return false;
|
||||
}
|
||||
return get_option('wc_composable_include_unpublished', 'no') === 'yes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available products based on criteria
|
||||
*
|
||||
@@ -104,21 +120,24 @@ class Product_Type extends \WC_Product {
|
||||
*/
|
||||
public function get_available_products() {
|
||||
$criteria = $this->get_selection_criteria();
|
||||
$include_unpublished = $this->should_include_unpublished();
|
||||
$args = [
|
||||
'post_type' => 'product',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'post_status' => $include_unpublished ? ['publish', 'draft', 'private'] : 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'tax_query' => [],
|
||||
];
|
||||
|
||||
// Exclude composable products from selection
|
||||
$args['meta_query'] = [
|
||||
// Exclude composable products using the product_type taxonomy
|
||||
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
||||
$args['tax_query'] = [
|
||||
'relation' => 'AND',
|
||||
[
|
||||
'key' => '_product_type',
|
||||
'value' => 'composable',
|
||||
'compare' => '!=',
|
||||
'taxonomy' => 'product_type',
|
||||
'field' => 'slug',
|
||||
'terms' => ['composable'],
|
||||
'operator' => 'NOT IN',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -148,10 +167,12 @@ class Product_Type extends \WC_Product {
|
||||
case 'sku':
|
||||
if (!empty($criteria['skus'])) {
|
||||
$skus = array_map('trim', explode(',', $criteria['skus']));
|
||||
$args['meta_query'][] = [
|
||||
$args['meta_query'] = [
|
||||
[
|
||||
'key' => '_sku',
|
||||
'value' => $skus,
|
||||
'compare' => 'IN',
|
||||
],
|
||||
];
|
||||
}
|
||||
break;
|
||||
@@ -163,13 +184,28 @@ class Product_Type extends \WC_Product {
|
||||
if ($query->have_posts()) {
|
||||
foreach ($query->posts as $post) {
|
||||
$product = wc_get_product($post->ID);
|
||||
if ($product && $product->is_in_stock() && $product->is_purchasable()) {
|
||||
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle variable products by including their variations
|
||||
if ($product->is_type('variable')) {
|
||||
$variation_ids = $product->get_children();
|
||||
foreach ($variation_ids as $variation_id) {
|
||||
$variation = wc_get_product($variation_id);
|
||||
if ($variation && ($include_unpublished || $variation->is_purchasable())) {
|
||||
$products[] = $variation;
|
||||
}
|
||||
}
|
||||
} elseif ($include_unpublished || $product->is_purchasable()) {
|
||||
$products[] = $product;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wp_reset_postdata();
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Stock Manager
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -14,7 +14,7 @@ defined('ABSPATH') || exit;
|
||||
*
|
||||
* Handles stock management for composable products
|
||||
*/
|
||||
class Stock_Manager {
|
||||
class StockManager {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
Binary file not shown.
Regular → Executable
+49
-29
@@ -91,7 +91,7 @@ msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
||||
msgid "Composable product"
|
||||
msgstr "Zusammenstellbares Produkt"
|
||||
|
||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
||||
#: includes/ProductSelector.php, templates/product-selector.twig
|
||||
msgid "Select Your Products"
|
||||
msgstr "Wählen Sie Ihre Produkte"
|
||||
|
||||
@@ -111,115 +111,115 @@ msgstr "Gesamtpreis:"
|
||||
msgid "Add to Cart"
|
||||
msgstr "In den Warenkorb"
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Please select at least one product."
|
||||
msgstr "Bitte wählen Sie mindestens ein Produkt aus."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "You can select a maximum of %d products."
|
||||
msgstr "Sie können maximal %d Produkte auswählen."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "One or more selected products are not available."
|
||||
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Selected Products"
|
||||
msgstr "Ausgewählte Produkte"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Composable Options"
|
||||
msgstr "Zusammenstellungsoptionen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Limit"
|
||||
msgstr "Auswahllimit"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||
msgstr "Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um den globalen Standard zu verwenden."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Pricing Mode"
|
||||
msgstr "Preismodus"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to calculate the price."
|
||||
msgstr "Wie der Preis berechnet wird."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Fixed Price"
|
||||
msgstr "Festpreis"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter the fixed price for this composable product."
|
||||
msgstr "Geben Sie den Festpreis für dieses zusammenstellbare Produkt ein."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Use global default"
|
||||
msgstr "Globalen Standard verwenden"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Criteria"
|
||||
msgstr "Auswahlkriterien"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to select available products."
|
||||
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Category"
|
||||
msgstr "Nach Kategorie"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Tag"
|
||||
msgstr "Nach Schlagwort"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By SKU"
|
||||
msgstr "Nach Artikelnummer"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Categories"
|
||||
msgstr "Kategorien auswählen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product categories to include."
|
||||
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Tags"
|
||||
msgstr "Schlagwörter auswählen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product tags to include."
|
||||
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Product SKUs"
|
||||
msgstr "Produkt-Artikelnummern"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter product SKUs separated by commas."
|
||||
msgstr "Geben Sie Produkt-Artikelnummern durch Kommas getrennt ein."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "SKU-1, SKU-2, SKU-3"
|
||||
msgstr "ART-1, ART-2, ART-3"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "\"%s\" is out of stock and cannot be selected."
|
||||
msgstr "\"%s\" ist nicht an Lager und kann nicht ausgewählt werden."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||
msgstr "Nur %2$d von \"%1$s\" sind an Lager verfügbar."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||
|
||||
@@ -242,3 +242,23 @@ msgstr "An Lager"
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfigurieren Sie die Produktkriterien im Admin-Bereich."
|
||||
|
||||
#: includes/Admin/Settings.php, includes/Admin/ProductData.php
|
||||
msgid "Include Non-Public Products"
|
||||
msgstr "Nicht-öffentliche Produkte einbeziehen"
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually."
|
||||
msgstr "Entwürfe und private Produkte in der Auswahl zusammenstellbarer Produkte anzeigen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung verkauft werden sollen, nicht einzeln."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Allow draft and private products in the selection. Useful when products should only be sold as part of a composition."
|
||||
msgstr "Entwürfe und private Produkte in der Auswahl zulassen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung verkauft werden sollen."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
Binary file not shown.
Regular → Executable
+49
-29
@@ -91,7 +91,7 @@ msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
||||
msgid "Composable product"
|
||||
msgstr "Zusammenstellbares Produkt"
|
||||
|
||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
||||
#: includes/ProductSelector.php, templates/product-selector.twig
|
||||
msgid "Select Your Products"
|
||||
msgstr "Wähle deine Produkte"
|
||||
|
||||
@@ -111,115 +111,115 @@ msgstr "Gesamtpreis:"
|
||||
msgid "Add to Cart"
|
||||
msgstr "In den Warenkorb"
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Please select at least one product."
|
||||
msgstr "Bitte wähle mindestens ein Produkt aus."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "You can select a maximum of %d products."
|
||||
msgstr "Du kannst maximal %d Produkte auswählen."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "One or more selected products are not available."
|
||||
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Selected Products"
|
||||
msgstr "Ausgewählte Produkte"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Composable Options"
|
||||
msgstr "Zusammenstellungsoptionen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Limit"
|
||||
msgstr "Auswahllimit"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||
msgstr "Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um den globalen Standard zu verwenden."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Pricing Mode"
|
||||
msgstr "Preismodus"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to calculate the price."
|
||||
msgstr "Wie der Preis berechnet wird."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Fixed Price"
|
||||
msgstr "Festpreis"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter the fixed price for this composable product."
|
||||
msgstr "Gib den Festpreis für dieses zusammenstellbare Produkt ein."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Use global default"
|
||||
msgstr "Globalen Standard verwenden"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Criteria"
|
||||
msgstr "Auswahlkriterien"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to select available products."
|
||||
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Category"
|
||||
msgstr "Nach Kategorie"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Tag"
|
||||
msgstr "Nach Schlagwort"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By SKU"
|
||||
msgstr "Nach Artikelnummer"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Categories"
|
||||
msgstr "Kategorien auswählen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product categories to include."
|
||||
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Tags"
|
||||
msgstr "Schlagwörter auswählen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product tags to include."
|
||||
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Product SKUs"
|
||||
msgstr "Produkt-Artikelnummern"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter product SKUs separated by commas."
|
||||
msgstr "Gib Produkt-Artikelnummern durch Kommas getrennt ein."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "SKU-1, SKU-2, SKU-3"
|
||||
msgstr "ART-1, ART-2, ART-3"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "\"%s\" is out of stock and cannot be selected."
|
||||
msgstr "\"%s\" ist nicht an Lager und kann nicht ausgewählt werden."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||
msgstr "Nur %2$d von \"%1$s\" sind an Lager verfügbar."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||
|
||||
@@ -242,3 +242,23 @@ msgstr "An Lager"
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfiguriere die Produktkriterien im Admin-Bereich."
|
||||
|
||||
#: includes/Admin/Settings.php, includes/Admin/ProductData.php
|
||||
msgid "Include Non-Public Products"
|
||||
msgstr "Nicht-öffentliche Produkte einbeziehen"
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually."
|
||||
msgstr "Entwürfe und private Produkte in der Auswahl zusammenstellbarer Produkte anzeigen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung verkauft werden sollen, nicht einzeln."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Allow draft and private products in the selection. Useful when products should only be sold as part of a composition."
|
||||
msgstr "Entwürfe und private Produkte in der Auswahl zulassen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung verkauft werden sollen."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
Binary file not shown.
Regular → Executable
+49
-29
@@ -91,7 +91,7 @@ msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
||||
msgid "Composable product"
|
||||
msgstr "Zusammenstellbares Produkt"
|
||||
|
||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
||||
#: includes/ProductSelector.php, templates/product-selector.twig
|
||||
msgid "Select Your Products"
|
||||
msgstr "Wählen Sie Ihre Produkte"
|
||||
|
||||
@@ -111,115 +111,115 @@ msgstr "Gesamtpreis:"
|
||||
msgid "Add to Cart"
|
||||
msgstr "In den Warenkorb"
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Please select at least one product."
|
||||
msgstr "Bitte wählen Sie mindestens ein Produkt aus."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "You can select a maximum of %d products."
|
||||
msgstr "Sie können maximal %d Produkte auswählen."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "One or more selected products are not available."
|
||||
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Selected Products"
|
||||
msgstr "Ausgewählte Produkte"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Composable Options"
|
||||
msgstr "Zusammenstellungsoptionen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Limit"
|
||||
msgstr "Auswahllimit"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||
msgstr "Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um den globalen Standard zu verwenden."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Pricing Mode"
|
||||
msgstr "Preismodus"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to calculate the price."
|
||||
msgstr "Wie der Preis berechnet wird."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Fixed Price"
|
||||
msgstr "Festpreis"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter the fixed price for this composable product."
|
||||
msgstr "Geben Sie den Festpreis für dieses zusammenstellbare Produkt ein."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Use global default"
|
||||
msgstr "Globalen Standard verwenden"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Criteria"
|
||||
msgstr "Auswahlkriterien"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to select available products."
|
||||
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Category"
|
||||
msgstr "Nach Kategorie"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Tag"
|
||||
msgstr "Nach Schlagwort"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By SKU"
|
||||
msgstr "Nach Artikelnummer"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Categories"
|
||||
msgstr "Kategorien auswählen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product categories to include."
|
||||
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Tags"
|
||||
msgstr "Schlagwörter auswählen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product tags to include."
|
||||
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Product SKUs"
|
||||
msgstr "Produkt-Artikelnummern"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter product SKUs separated by commas."
|
||||
msgstr "Geben Sie Produkt-Artikelnummern durch Kommas getrennt ein."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "SKU-1, SKU-2, SKU-3"
|
||||
msgstr "ART-1, ART-2, ART-3"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "\"%s\" is out of stock and cannot be selected."
|
||||
msgstr "\"%s\" ist nicht auf Lager und kann nicht ausgewählt werden."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||
msgstr "Nur %2$d von \"%1$s\" sind auf Lager verfügbar."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||
|
||||
@@ -242,3 +242,23 @@ msgstr "Auf Lager"
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfigurieren Sie die Produktkriterien im Admin-Bereich."
|
||||
|
||||
#: includes/Admin/Settings.php, includes/Admin/ProductData.php
|
||||
msgid "Include Non-Public Products"
|
||||
msgstr "Nicht-öffentliche Produkte einbeziehen"
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually."
|
||||
msgstr "Entwürfe und private Produkte in der Auswahl zusammenstellbarer Produkte anzeigen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung verkauft werden sollen, nicht einzeln."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Allow draft and private products in the selection. Useful when products should only be sold as part of a composition."
|
||||
msgstr "Entwürfe und private Produkte in der Auswahl zulassen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung verkauft werden sollen."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
Binary file not shown.
Regular → Executable
+49
-29
@@ -91,7 +91,7 @@ msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
||||
msgid "Composable product"
|
||||
msgstr "Zusammenstellbares Produkt"
|
||||
|
||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
||||
#: includes/ProductSelector.php, templates/product-selector.twig
|
||||
msgid "Select Your Products"
|
||||
msgstr "Wähle deine Produkte"
|
||||
|
||||
@@ -111,115 +111,115 @@ msgstr "Gesamtpreis:"
|
||||
msgid "Add to Cart"
|
||||
msgstr "In den Warenkorb"
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Please select at least one product."
|
||||
msgstr "Bitte wähle mindestens ein Produkt aus."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "You can select a maximum of %d products."
|
||||
msgstr "Du kannst maximal %d Produkte auswählen."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "One or more selected products are not available."
|
||||
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Selected Products"
|
||||
msgstr "Ausgewählte Produkte"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Composable Options"
|
||||
msgstr "Zusammenstellungsoptionen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Limit"
|
||||
msgstr "Auswahllimit"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||
msgstr "Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um den globalen Standard zu verwenden."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Pricing Mode"
|
||||
msgstr "Preismodus"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to calculate the price."
|
||||
msgstr "Wie der Preis berechnet wird."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Fixed Price"
|
||||
msgstr "Festpreis"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter the fixed price for this composable product."
|
||||
msgstr "Gib den Festpreis für dieses zusammenstellbare Produkt ein."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Use global default"
|
||||
msgstr "Globalen Standard verwenden"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Criteria"
|
||||
msgstr "Auswahlkriterien"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to select available products."
|
||||
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Category"
|
||||
msgstr "Nach Kategorie"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Tag"
|
||||
msgstr "Nach Schlagwort"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By SKU"
|
||||
msgstr "Nach Artikelnummer"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Categories"
|
||||
msgstr "Kategorien auswählen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product categories to include."
|
||||
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Tags"
|
||||
msgstr "Schlagwörter auswählen"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product tags to include."
|
||||
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Product SKUs"
|
||||
msgstr "Produkt-Artikelnummern"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter product SKUs separated by commas."
|
||||
msgstr "Gib Produkt-Artikelnummern durch Kommas getrennt ein."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "SKU-1, SKU-2, SKU-3"
|
||||
msgstr "ART-1, ART-2, ART-3"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "\"%s\" is out of stock and cannot be selected."
|
||||
msgstr "\"%s\" ist nicht auf Lager und kann nicht ausgewählt werden."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||
msgstr "Nur %2$d von \"%1$s\" sind auf Lager verfügbar."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||
|
||||
@@ -242,3 +242,23 @@ msgstr "Auf Lager"
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfiguriere die Produktkriterien im Admin-Bereich."
|
||||
|
||||
#: includes/Admin/Settings.php, includes/Admin/ProductData.php
|
||||
msgid "Include Non-Public Products"
|
||||
msgstr "Nicht-öffentliche Produkte einbeziehen"
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually."
|
||||
msgstr "Entwürfe und private Produkte in der Auswahl zusammenstellbarer Produkte anzeigen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung verkauft werden sollen, nicht einzeln."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Allow draft and private products in the selection. Useful when products should only be sold as part of a composition."
|
||||
msgstr "Entwürfe und private Produkte in der Auswahl zulassen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung verkauft werden sollen."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
Binary file not shown.
Regular → Executable
+49
-29
@@ -91,7 +91,7 @@ msgstr "Afficher le prix total pendant que les clients font leurs sélections."
|
||||
msgid "Composable product"
|
||||
msgstr "Produit composable"
|
||||
|
||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
||||
#: includes/ProductSelector.php, templates/product-selector.twig
|
||||
msgid "Select Your Products"
|
||||
msgstr "Sélectionnez vos produits"
|
||||
|
||||
@@ -111,115 +111,115 @@ msgstr "Prix total :"
|
||||
msgid "Add to Cart"
|
||||
msgstr "Ajouter au panier"
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Please select at least one product."
|
||||
msgstr "Veuillez sélectionner au moins un produit."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "You can select a maximum of %d products."
|
||||
msgstr "Vous pouvez sélectionner un maximum de %d produits."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "One or more selected products are not available."
|
||||
msgstr "Un ou plusieurs produits sélectionnés ne sont pas disponibles."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Selected Products"
|
||||
msgstr "Produits sélectionnés"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Composable Options"
|
||||
msgstr "Options de composition"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Limit"
|
||||
msgstr "Limite de sélection"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||
msgstr "Nombre maximum d'articles que les clients peuvent sélectionner. Laisser vide pour utiliser la valeur par défaut globale."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Pricing Mode"
|
||||
msgstr "Mode de tarification"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to calculate the price."
|
||||
msgstr "Comment calculer le prix."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Fixed Price"
|
||||
msgstr "Prix fixe"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter the fixed price for this composable product."
|
||||
msgstr "Entrez le prix fixe pour ce produit composable."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Use global default"
|
||||
msgstr "Utiliser la valeur par défaut globale"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Criteria"
|
||||
msgstr "Critères de sélection"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to select available products."
|
||||
msgstr "Comment sélectionner les produits disponibles."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Category"
|
||||
msgstr "Par catégorie"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Tag"
|
||||
msgstr "Par étiquette"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By SKU"
|
||||
msgstr "Par référence"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Categories"
|
||||
msgstr "Sélectionner les catégories"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product categories to include."
|
||||
msgstr "Sélectionner les catégories de produits à inclure."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Tags"
|
||||
msgstr "Sélectionner les étiquettes"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product tags to include."
|
||||
msgstr "Sélectionner les étiquettes de produits à inclure."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Product SKUs"
|
||||
msgstr "Références des produits"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter product SKUs separated by commas."
|
||||
msgstr "Entrez les références des produits séparées par des virgules."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "SKU-1, SKU-2, SKU-3"
|
||||
msgstr "REF-1, REF-2, REF-3"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "\"%s\" is out of stock and cannot be selected."
|
||||
msgstr "\"%s\" est en rupture de stock et ne peut pas être sélectionné."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||
msgstr "Seulement %2$d de \"%1$s\" sont disponibles en stock."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||
msgstr "Stock réduit pour \"%1$s\": -%2$d (restant: %3$d)"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||
msgstr "Stock restauré pour \"%1$s\": +%2$d (total: %3$d)"
|
||||
|
||||
@@ -242,3 +242,23 @@ msgstr "En stock"
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Aucun produit disponible pour la sélection. Veuillez configurer les critères de produit dans le panneau d'administration."
|
||||
|
||||
#: includes/Admin/Settings.php, includes/Admin/ProductData.php
|
||||
msgid "Include Non-Public Products"
|
||||
msgstr "Inclure les produits non publics"
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually."
|
||||
msgstr "Autoriser les brouillons et les produits privés dans les sélections de produits composables. Utile lorsque les produits ne doivent être vendus que dans le cadre d'une composition, pas individuellement."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Allow draft and private products in the selection. Useful when products should only be sold as part of a composition."
|
||||
msgstr "Autoriser les brouillons et les produits privés dans la sélection. Utile lorsque les produits ne doivent être vendus que dans le cadre d'une composition."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Yes"
|
||||
msgstr "Oui"
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "No"
|
||||
msgstr "Non"
|
||||
Binary file not shown.
Regular → Executable
+49
-29
@@ -91,7 +91,7 @@ msgstr "Visualizzare il prezzo totale mentre i clienti effettuano le selezioni."
|
||||
msgid "Composable product"
|
||||
msgstr "Prodotto componibile"
|
||||
|
||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
||||
#: includes/ProductSelector.php, templates/product-selector.twig
|
||||
msgid "Select Your Products"
|
||||
msgstr "Seleziona i tuoi prodotti"
|
||||
|
||||
@@ -111,115 +111,115 @@ msgstr "Prezzo totale:"
|
||||
msgid "Add to Cart"
|
||||
msgstr "Aggiungi al carrello"
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Please select at least one product."
|
||||
msgstr "Seleziona almeno un prodotto."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "You can select a maximum of %d products."
|
||||
msgstr "Puoi selezionare un massimo di %d prodotti."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "One or more selected products are not available."
|
||||
msgstr "Uno o più prodotti selezionati non sono disponibili."
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Selected Products"
|
||||
msgstr "Prodotti selezionati"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Composable Options"
|
||||
msgstr "Opzioni di composizione"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Limit"
|
||||
msgstr "Limite di selezione"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||
msgstr "Numero massimo di articoli che i clienti possono selezionare. Lasciare vuoto per utilizzare il valore predefinito globale."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Pricing Mode"
|
||||
msgstr "Modalità di prezzo"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to calculate the price."
|
||||
msgstr "Come calcolare il prezzo."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Fixed Price"
|
||||
msgstr "Prezzo fisso"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter the fixed price for this composable product."
|
||||
msgstr "Inserisci il prezzo fisso per questo prodotto componibile."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Use global default"
|
||||
msgstr "Usa predefinito globale"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Criteria"
|
||||
msgstr "Criteri di selezione"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to select available products."
|
||||
msgstr "Come selezionare i prodotti disponibili."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Category"
|
||||
msgstr "Per categoria"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Tag"
|
||||
msgstr "Per etichetta"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By SKU"
|
||||
msgstr "Per codice articolo"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Categories"
|
||||
msgstr "Seleziona categorie"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product categories to include."
|
||||
msgstr "Selezionare le categorie di prodotti da includere."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Tags"
|
||||
msgstr "Seleziona etichette"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product tags to include."
|
||||
msgstr "Selezionare le etichette dei prodotti da includere."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Product SKUs"
|
||||
msgstr "Codici articolo prodotti"
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter product SKUs separated by commas."
|
||||
msgstr "Inserire i codici articolo dei prodotti separati da virgole."
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "SKU-1, SKU-2, SKU-3"
|
||||
msgstr "COD-1, COD-2, COD-3"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "\"%s\" is out of stock and cannot be selected."
|
||||
msgstr "\"%s\" è esaurito e non può essere selezionato."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||
msgstr "Solo %2$d di \"%1$s\" sono disponibili in magazzino."
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||
msgstr "Giacenza ridotta per \"%1$s\": -%2$d (rimanenti: %3$d)"
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||
msgstr "Giacenza ripristinata per \"%1$s\": +%2$d (totale: %3$d)"
|
||||
|
||||
@@ -242,3 +242,23 @@ msgstr "Disponibile"
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Nessun prodotto disponibile per la selezione. Si prega di configurare i criteri del prodotto nel pannello di amministrazione."
|
||||
|
||||
#: includes/Admin/Settings.php, includes/Admin/ProductData.php
|
||||
msgid "Include Non-Public Products"
|
||||
msgstr "Includi prodotti non pubblici"
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually."
|
||||
msgstr "Consenti la visualizzazione di bozze e prodotti privati nelle selezioni dei prodotti componibili. Utile quando i prodotti devono essere venduti solo come parte di una composizione, non singolarmente."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Allow draft and private products in the selection. Useful when products should only be sold as part of a composition."
|
||||
msgstr "Consenti bozze e prodotti privati nella selezione. Utile quando i prodotti devono essere venduti solo come parte di una composizione."
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Yes"
|
||||
msgstr "Sì"
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
Regular → Executable
+49
-29
@@ -90,7 +90,7 @@ msgstr ""
|
||||
msgid "Composable product"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
||||
#: includes/ProductSelector.php, templates/product-selector.twig
|
||||
msgid "Select Your Products"
|
||||
msgstr ""
|
||||
|
||||
@@ -110,115 +110,115 @@ msgstr ""
|
||||
msgid "Add to Cart"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Please select at least one product."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "You can select a maximum of %d products."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "One or more selected products are not available."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
#: includes/CartHandler.php
|
||||
msgid "Selected Products"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Composable Options"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Limit"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Pricing Mode"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to calculate the price."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Fixed Price"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter the fixed price for this composable product."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Use global default"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Selection Criteria"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "How to select available products."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Category"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By Tag"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "By SKU"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Categories"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product categories to include."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select Tags"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Select product tags to include."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Product SKUs"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Enter product SKUs separated by commas."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "SKU-1, SKU-2, SKU-3"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "\"%s\" is out of stock and cannot be selected."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Stock_Manager.php
|
||||
#: includes/StockManager.php
|
||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||
msgstr ""
|
||||
|
||||
@@ -241,3 +241,23 @@ msgstr ""
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php, includes/Admin/ProductData.php
|
||||
msgid "Include Non-Public Products"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Allow draft and private products in the selection. Useful when products should only be sold as part of a composition."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/ProductData.php
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
verbose="true"
|
||||
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
failOnRisky="false"
|
||||
failOnWarning="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">includes</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
</phpunit>
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
aec3bae001f0013322a73fa941169688 wc-composable-product-v1.0.0.zip
|
||||
@@ -1 +0,0 @@
|
||||
4a0f7ec2171aeabfdfe155419fd6124f35f3e14501ee2ca324bbab447259a8bb wc-composable-product-v1.0.0.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
0a60816bbc5a01c0057c1ffa72679d93 releases/wc-composable-product-v1.1.0.zip
|
||||
@@ -1 +0,0 @@
|
||||
645fdd68aca95cba77d961f3a48d41b9c12b3d17552572b7c039575dcfcab693 releases/wc-composable-product-v1.1.0.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
db09928aea6fffbf9c2e754d2264f2bc wc-composable-product-v1.1.1.zip
|
||||
@@ -1 +0,0 @@
|
||||
761eef69da910ecfdb20ceeed70b5d0381c7cab895e81a040d132cb0f88d749b wc-composable-product-v1.1.1.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
37cef191778b448dcbd2ae10141f64c6 wc-composable-product-v1.1.2.zip
|
||||
@@ -1 +0,0 @@
|
||||
191eae035b34ce8b33b90cf9d85ed54e493c1b471cda0efe5c992a512e91cc36 wc-composable-product-v1.1.2.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
9bbed416019a796b4d4a5ef72e016e1f wc-composable-product-v1.1.3.zip
|
||||
@@ -1 +0,0 @@
|
||||
0ca23ca12570f0e9c518514ffc5209d78c76c3295954d10ec74a28013a762956 wc-composable-product-v1.1.3.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
eae384e342450abd4ac83af0266ac764 wc-composable-product-v1.1.6.zip
|
||||
@@ -1 +0,0 @@
|
||||
d64f4f5f1a00d392989cb613780e5726106a08c6aace08e0c74c80553a0b0f1e wc-composable-product-v1.1.6.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
871fbb3b910380c0e43bcf1538408eda releases/wc-composable-product-v1.1.7.zip
|
||||
@@ -1 +0,0 @@
|
||||
866e7dd34431f4c881629fd8b59ddd3a27c7a45b7324a3d88cd064a3e01c1b83 releases/wc-composable-product-v1.1.7.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
78eee5eee4762c308c5d37d1aac06b04 wc-composable-product-v1.1.8.zip
|
||||
@@ -1 +0,0 @@
|
||||
d7d06e2a5d336609249f803b681cdf270dbe60d6fc28bdd6c451c6744d2fdab6 wc-composable-product-v1.1.8.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
a5b08f3613d1b1e8aba0c2b7b82a1582 wc-composable-product-v1.1.9.zip
|
||||
@@ -1 +0,0 @@
|
||||
f9fc497c0531c7ea828e164137f3db6e0a2755b899690dfb7d6411baf0c7a65a wc-composable-product-v1.1.9.zip
|
||||
Regular → Executable
File mode changed.
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Base Test Case
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
|
||||
use Brain\Monkey;
|
||||
|
||||
/**
|
||||
* Base test case with Brain Monkey and Mockery integration.
|
||||
*
|
||||
* All test classes should extend this instead of PHPUnit\Framework\TestCase.
|
||||
*/
|
||||
abstract class TestCase extends PHPUnitTestCase
|
||||
{
|
||||
use MockeryPHPUnitIntegration;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Monkey\setUp();
|
||||
|
||||
// Stub common WordPress translation functions (__(), _e(), esc_html__(), etc.)
|
||||
Monkey\Functions\stubTranslationFunctions();
|
||||
|
||||
// Stub common WordPress escaping functions (esc_html(), esc_attr(), etc.)
|
||||
Monkey\Functions\stubEscapeFunctions();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Monkey\tearDown();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Mockery mock of WC_Product with sensible defaults.
|
||||
*
|
||||
* @param array $overrides Method return value overrides
|
||||
* @return \Mockery\MockInterface
|
||||
*/
|
||||
protected function createProductMock(array $overrides = []): \Mockery\MockInterface
|
||||
{
|
||||
$defaults = [
|
||||
'get_id' => 100,
|
||||
'get_name' => 'Test Product',
|
||||
'get_type' => 'simple',
|
||||
'get_price' => '10.00',
|
||||
'get_regular_price' => '10.00',
|
||||
'get_price_html' => '<span>$10.00</span>',
|
||||
'get_permalink' => 'https://example.com/product/test',
|
||||
'get_image_id' => 1,
|
||||
'get_stock_quantity' => null,
|
||||
'get_stock_status' => 'instock',
|
||||
'is_purchasable' => true,
|
||||
'is_in_stock' => true,
|
||||
'managing_stock' => false,
|
||||
'backorders_allowed' => false,
|
||||
];
|
||||
|
||||
$config = array_merge($defaults, $overrides);
|
||||
$mock = \Mockery::mock('WC_Product');
|
||||
|
||||
foreach ($config as $method => $return) {
|
||||
$mock->shouldReceive($method)->andReturn($return)->byDefault();
|
||||
}
|
||||
|
||||
return $mock;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin ProductData Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit\Admin;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\Admin\ProductData;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class ProductDataTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_POST = [];
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testConstructor_RegistersExpectedHooks(): void
|
||||
{
|
||||
$productData = new ProductData();
|
||||
|
||||
self::assertNotFalse(has_filter('woocommerce_product_data_tabs', 'Magdev\WcComposableProduct\Admin\ProductData->add_product_data_tab()'));
|
||||
self::assertNotFalse(has_action('woocommerce_product_data_panels', 'Magdev\WcComposableProduct\Admin\ProductData->add_product_data_panel()'));
|
||||
self::assertNotFalse(has_action('woocommerce_process_product_meta_composable', 'Magdev\WcComposableProduct\Admin\ProductData->save_product_data()'));
|
||||
self::assertNotFalse(has_action('woocommerce_product_options_general_product_data', 'Magdev\WcComposableProduct\Admin\ProductData->add_general_fields()'));
|
||||
}
|
||||
|
||||
public function testAddProductDataTab_AddsComposableTab(): void
|
||||
{
|
||||
$productData = new ProductData();
|
||||
$tabs = $productData->add_product_data_tab([]);
|
||||
|
||||
$this->assertArrayHasKey('composable', $tabs);
|
||||
$this->assertSame('composable_product_data', $tabs['composable']['target']);
|
||||
$this->assertContains('show_if_composable', $tabs['composable']['class']);
|
||||
$this->assertSame(21, $tabs['composable']['priority']);
|
||||
}
|
||||
|
||||
public function testSaveProductData_SavesAllFields(): void
|
||||
{
|
||||
$_POST = [
|
||||
'_composable_selection_limit' => '5',
|
||||
'_composable_pricing_mode' => 'fixed',
|
||||
'_composable_include_unpublished' => 'yes',
|
||||
'_composable_criteria_type' => 'tag',
|
||||
'_composable_categories' => ['1', '2'],
|
||||
'_composable_tags' => ['3', '4'],
|
||||
'_composable_skus' => 'SKU-1, SKU-2',
|
||||
];
|
||||
|
||||
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
Functions\expect('sanitize_text_field')->andReturnUsing(function ($val) {
|
||||
return $val;
|
||||
});
|
||||
Functions\expect('sanitize_textarea_field')->andReturnUsing(function ($val) {
|
||||
return $val;
|
||||
});
|
||||
|
||||
Functions\expect('update_post_meta')->times(7);
|
||||
|
||||
$productData = new ProductData();
|
||||
$productData->save_product_data(42);
|
||||
}
|
||||
|
||||
public function testSaveProductData_DefaultsWhenPostEmpty(): void
|
||||
{
|
||||
// No POST data at all
|
||||
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
Functions\expect('sanitize_text_field')->andReturnUsing(function ($val) {
|
||||
return $val;
|
||||
});
|
||||
Functions\expect('sanitize_textarea_field')->andReturnUsing(function ($val) {
|
||||
return $val;
|
||||
});
|
||||
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_selection_limit', \Mockery::any())->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_pricing_mode', '')->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_include_unpublished', '')->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_criteria_type', 'category')->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_categories', [])->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_tags', [])->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_skus', '')->once();
|
||||
|
||||
$productData = new ProductData();
|
||||
$productData->save_product_data(42);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Settings Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit\Admin;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\Admin\Settings;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class SettingsTest extends TestCase
|
||||
{
|
||||
public function testConstructor_SetsIdAndLabel(): void
|
||||
{
|
||||
$settings = new Settings();
|
||||
|
||||
$this->assertSame('composable_products', $settings->get_id());
|
||||
}
|
||||
|
||||
public function testGetSettings_ReturnsExpectedFieldIds(): void
|
||||
{
|
||||
Functions\expect('apply_filters')
|
||||
->once()
|
||||
->with('wc_composable_settings', \Mockery::type('array'))
|
||||
->andReturnUsing(function ($hook, $settings) {
|
||||
return $settings;
|
||||
});
|
||||
|
||||
$settings = new Settings();
|
||||
$fields = $settings->get_settings();
|
||||
|
||||
// Extract all field IDs
|
||||
$ids = array_column($fields, 'id');
|
||||
|
||||
$this->assertContains('wc_composable_settings', $ids);
|
||||
$this->assertContains('wc_composable_default_limit', $ids);
|
||||
$this->assertContains('wc_composable_default_pricing', $ids);
|
||||
$this->assertContains('wc_composable_include_unpublished', $ids);
|
||||
$this->assertContains('wc_composable_show_images', $ids);
|
||||
$this->assertContains('wc_composable_show_prices', $ids);
|
||||
$this->assertContains('wc_composable_show_total', $ids);
|
||||
}
|
||||
|
||||
public function testGetSettings_HasCorrectFieldTypes(): void
|
||||
{
|
||||
Functions\expect('apply_filters')
|
||||
->once()
|
||||
->andReturnUsing(function ($hook, $settings) {
|
||||
return $settings;
|
||||
});
|
||||
|
||||
$settings = new Settings();
|
||||
$fields = $settings->get_settings();
|
||||
|
||||
// Index fields by ID for easy lookup
|
||||
$indexed = [];
|
||||
foreach ($fields as $field) {
|
||||
if (isset($field['id'])) {
|
||||
$indexed[$field['id']] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame('number', $indexed['wc_composable_default_limit']['type']);
|
||||
$this->assertSame('select', $indexed['wc_composable_default_pricing']['type']);
|
||||
$this->assertSame('checkbox', $indexed['wc_composable_include_unpublished']['type']);
|
||||
$this->assertSame('checkbox', $indexed['wc_composable_show_images']['type']);
|
||||
}
|
||||
|
||||
public function testGetSettings_AppliesFilter(): void
|
||||
{
|
||||
Functions\expect('apply_filters')
|
||||
->once()
|
||||
->with('wc_composable_settings', \Mockery::type('array'))
|
||||
->andReturnUsing(function ($hook, $settings) {
|
||||
return $settings;
|
||||
});
|
||||
|
||||
$settings = new Settings();
|
||||
$settings->get_settings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* CartHandler Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\CartHandler;
|
||||
use Brain\Monkey\Functions;
|
||||
use Brain\Monkey\Actions;
|
||||
use Brain\Monkey\Filters;
|
||||
|
||||
class CartHandlerTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Clean up POST superglobal between tests
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_POST = [];
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testConstructor_RegistersExpectedHooks(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
|
||||
self::assertNotFalse(has_filter('woocommerce_add_to_cart_validation', 'Magdev\WcComposableProduct\CartHandler->validate_add_to_cart()'));
|
||||
self::assertNotFalse(has_filter('woocommerce_add_cart_item_data', 'Magdev\WcComposableProduct\CartHandler->add_cart_item_data()'));
|
||||
self::assertNotFalse(has_filter('woocommerce_get_cart_item_from_session', 'Magdev\WcComposableProduct\CartHandler->get_cart_item_from_session()'));
|
||||
self::assertNotFalse(has_filter('woocommerce_get_item_data', 'Magdev\WcComposableProduct\CartHandler->display_cart_item_data()'));
|
||||
self::assertNotFalse(has_action('woocommerce_before_calculate_totals', 'Magdev\WcComposableProduct\CartHandler->calculate_cart_item_price()'));
|
||||
self::assertNotFalse(has_action('woocommerce_single_product_summary', 'Magdev\WcComposableProduct\CartHandler->render_product_selector()'));
|
||||
self::assertNotFalse(has_filter('woocommerce_is_purchasable', 'Magdev\WcComposableProduct\CartHandler->hide_default_add_to_cart()'));
|
||||
}
|
||||
|
||||
public function testHideDefaultAddToCart_ReturnsFalseForComposable(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||
|
||||
$result = $handler->hide_default_add_to_cart(true, $product);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testHideDefaultAddToCart_PassesThroughForSimple(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||
|
||||
$result = $handler->hide_default_add_to_cart(true, $product);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateAddToCart_PassesThroughForNonComposable(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateAddToCart_ReturnsFalseWhenNoProductsSelected(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
Functions\expect('wc_add_notice')->once();
|
||||
|
||||
$_POST['composable_products'] = [];
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testValidateAddToCart_ReturnsFalseWhenNoPostData(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
Functions\expect('wc_add_notice')->once();
|
||||
|
||||
// No $_POST['composable_products'] at all
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testAddCartItemData_AddsSelectionsForComposable(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
|
||||
$_POST['composable_products'] = ['101', '102'];
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->add_cart_item_data([], 1);
|
||||
|
||||
$this->assertArrayHasKey('composable_products', $result);
|
||||
$this->assertSame([101, 102], $result['composable_products']);
|
||||
$this->assertArrayHasKey('unique_key', $result);
|
||||
}
|
||||
|
||||
public function testAddCartItemData_PassesThroughForNonComposable(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->add_cart_item_data(['existing' => 'data'], 1);
|
||||
|
||||
$this->assertSame(['existing' => 'data'], $result);
|
||||
}
|
||||
|
||||
public function testGetCartItemFromSession_RestoresComposableProducts(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->get_cart_item_from_session(
|
||||
['data' => 'test'],
|
||||
['composable_products' => [101, 102]]
|
||||
);
|
||||
|
||||
$this->assertSame([101, 102], $result['composable_products']);
|
||||
}
|
||||
|
||||
public function testGetCartItemFromSession_PassesThroughWithoutComposableData(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->get_cart_item_from_session(
|
||||
['data' => 'test'],
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertArrayNotHasKey('composable_products', $result);
|
||||
}
|
||||
|
||||
public function testDisplayCartItemData_FormatsProductNames(): void
|
||||
{
|
||||
$mock1 = $this->createProductMock(['get_name' => 'Product A']);
|
||||
$mock2 = $this->createProductMock(['get_name' => 'Product B']);
|
||||
|
||||
Functions\expect('wc_get_product')
|
||||
->andReturnUsing(function ($id) use ($mock1, $mock2) {
|
||||
return match ($id) {
|
||||
101 => $mock1,
|
||||
102 => $mock2,
|
||||
default => false,
|
||||
};
|
||||
});
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->display_cart_item_data(
|
||||
[],
|
||||
['composable_products' => [101, 102]]
|
||||
);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertStringContainsString('Product A', $result[0]['value']);
|
||||
$this->assertStringContainsString('Product B', $result[0]['value']);
|
||||
}
|
||||
|
||||
public function testDisplayCartItemData_ReturnsEmptyForNonComposable(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->display_cart_item_data([], []);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\Plugin;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class PluginTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Reset the singleton instance between tests
|
||||
$reflection = new \ReflectionClass(Plugin::class);
|
||||
$property = $reflection->getProperty('instance');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, null);
|
||||
}
|
||||
|
||||
public function testInstance_ReturnsSingleton(): void
|
||||
{
|
||||
$instance1 = Plugin::instance();
|
||||
$instance2 = Plugin::instance();
|
||||
|
||||
$this->assertSame($instance1, $instance2);
|
||||
}
|
||||
|
||||
public function testInstance_ReturnsPluginClass(): void
|
||||
{
|
||||
$instance = Plugin::instance();
|
||||
|
||||
$this->assertInstanceOf(Plugin::class, $instance);
|
||||
}
|
||||
|
||||
public function testAddProductType_AddsComposableToTypes(): void
|
||||
{
|
||||
$plugin = Plugin::instance();
|
||||
$types = $plugin->add_product_type([]);
|
||||
|
||||
$this->assertArrayHasKey('composable', $types);
|
||||
}
|
||||
|
||||
public function testProductClass_ReturnsCustomClassForComposable(): void
|
||||
{
|
||||
$plugin = Plugin::instance();
|
||||
$class = $plugin->product_class('WC_Product', 'composable');
|
||||
|
||||
$this->assertSame('Magdev\WcComposableProduct\ProductType', $class);
|
||||
}
|
||||
|
||||
public function testProductClass_PassesThroughForOtherTypes(): void
|
||||
{
|
||||
$plugin = Plugin::instance();
|
||||
$class = $plugin->product_class('WC_Product', 'simple');
|
||||
|
||||
$this->assertSame('WC_Product', $class);
|
||||
}
|
||||
|
||||
public function testGetTwig_ReturnsTwigEnvironment(): void
|
||||
{
|
||||
$plugin = Plugin::instance();
|
||||
|
||||
$this->assertInstanceOf(\Twig\Environment::class, $plugin->get_twig());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/**
|
||||
* ProductType Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\ProductType;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class ProductTypeTest extends TestCase
|
||||
{
|
||||
private function createProductType(array $meta = []): ProductType
|
||||
{
|
||||
$product = new ProductType();
|
||||
foreach ($meta as $key => $value) {
|
||||
$product->update_meta_data($key, $value);
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
public function testGetType_ReturnsComposable(): void
|
||||
{
|
||||
$product = $this->createProductType();
|
||||
$this->assertSame('composable', $product->get_type());
|
||||
}
|
||||
|
||||
public function testIsPurchasable_ReturnsTrue(): void
|
||||
{
|
||||
$product = $this->createProductType();
|
||||
$this->assertTrue($product->is_purchasable());
|
||||
}
|
||||
|
||||
public function testIsSoldIndividually_ReturnsTrue(): void
|
||||
{
|
||||
$product = $this->createProductType();
|
||||
$this->assertTrue($product->is_sold_individually());
|
||||
}
|
||||
|
||||
public function testGetSelectionLimit_UsesProductMeta(): void
|
||||
{
|
||||
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
|
||||
$product = $this->createProductType(['_composable_selection_limit' => '3']);
|
||||
$this->assertSame(3, $product->get_selection_limit());
|
||||
}
|
||||
|
||||
public function testGetSelectionLimit_FallsBackToGlobalDefault(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_default_limit', 5)
|
||||
->andReturn(7);
|
||||
|
||||
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertSame(7, $product->get_selection_limit());
|
||||
}
|
||||
|
||||
public function testGetSelectionLimit_FallsBackToHardDefault(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_default_limit', 5)
|
||||
->andReturn(5);
|
||||
|
||||
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertSame(5, $product->get_selection_limit());
|
||||
}
|
||||
|
||||
public function testGetPricingMode_UsesProductMeta(): void
|
||||
{
|
||||
$product = $this->createProductType(['_composable_pricing_mode' => 'fixed']);
|
||||
$this->assertSame('fixed', $product->get_pricing_mode());
|
||||
}
|
||||
|
||||
public function testGetPricingMode_FallsBackToGlobalDefault(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_default_pricing', 'sum')
|
||||
->andReturn('sum');
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertSame('sum', $product->get_pricing_mode());
|
||||
}
|
||||
|
||||
public function testShouldIncludeUnpublished_PerProductYes(): void
|
||||
{
|
||||
$product = $this->createProductType(['_composable_include_unpublished' => 'yes']);
|
||||
$this->assertTrue($product->should_include_unpublished());
|
||||
}
|
||||
|
||||
public function testShouldIncludeUnpublished_PerProductNo(): void
|
||||
{
|
||||
$product = $this->createProductType(['_composable_include_unpublished' => 'no']);
|
||||
$this->assertFalse($product->should_include_unpublished());
|
||||
}
|
||||
|
||||
public function testShouldIncludeUnpublished_FallsBackToGlobalYes(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_include_unpublished', 'no')
|
||||
->andReturn('yes');
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertTrue($product->should_include_unpublished());
|
||||
}
|
||||
|
||||
public function testShouldIncludeUnpublished_FallsBackToGlobalNo(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_include_unpublished', 'no')
|
||||
->andReturn('no');
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertFalse($product->should_include_unpublished());
|
||||
}
|
||||
|
||||
public function testGetSelectionCriteria_DefaultsToCategory(): void
|
||||
{
|
||||
$product = $this->createProductType();
|
||||
$criteria = $product->get_selection_criteria();
|
||||
|
||||
$this->assertSame('category', $criteria['type']);
|
||||
$this->assertSame([], $criteria['categories']);
|
||||
$this->assertSame([], $criteria['tags']);
|
||||
$this->assertSame('', $criteria['skus']);
|
||||
}
|
||||
|
||||
public function testGetSelectionCriteria_UsesProductMeta(): void
|
||||
{
|
||||
$product = $this->createProductType([
|
||||
'_composable_criteria_type' => 'tag',
|
||||
'_composable_tags' => [5, 10],
|
||||
]);
|
||||
$criteria = $product->get_selection_criteria();
|
||||
|
||||
$this->assertSame('tag', $criteria['type']);
|
||||
$this->assertSame([5, 10], $criteria['tags']);
|
||||
}
|
||||
|
||||
public function testCalculateComposedPrice_FixedMode(): void
|
||||
{
|
||||
$product = $this->createProductType(['_composable_pricing_mode' => 'fixed']);
|
||||
|
||||
// Set regular price via the stub's data property
|
||||
$reflection = new \ReflectionClass($product);
|
||||
$dataProp = $reflection->getProperty('data');
|
||||
$dataProp->setAccessible(true);
|
||||
$data = $dataProp->getValue($product);
|
||||
$data['regular_price'] = '25.00';
|
||||
$dataProp->setValue($product, $data);
|
||||
|
||||
$price = $product->calculate_composed_price([101, 102]);
|
||||
$this->assertSame(25.0, $price);
|
||||
}
|
||||
|
||||
public function testCalculateComposedPrice_SumMode(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->with('wc_composable_default_pricing', 'sum')
|
||||
->andReturn('sum');
|
||||
|
||||
$mock1 = $this->createProductMock(['get_price' => '5.00']);
|
||||
$mock2 = $this->createProductMock(['get_price' => '7.50']);
|
||||
|
||||
Functions\expect('wc_get_product')
|
||||
->andReturnUsing(function ($id) use ($mock1, $mock2) {
|
||||
return match ($id) {
|
||||
101 => $mock1,
|
||||
102 => $mock2,
|
||||
default => false,
|
||||
};
|
||||
});
|
||||
|
||||
$product = $this->createProductType();
|
||||
$price = $product->calculate_composed_price([101, 102]);
|
||||
|
||||
$this->assertSame(12.5, $price);
|
||||
}
|
||||
|
||||
public function testCalculateComposedPrice_SumMode_SkipsInvalidProducts(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->with('wc_composable_default_pricing', 'sum')
|
||||
->andReturn('sum');
|
||||
|
||||
$mock1 = $this->createProductMock(['get_price' => '5.00']);
|
||||
|
||||
Functions\expect('wc_get_product')
|
||||
->andReturnUsing(function ($id) use ($mock1) {
|
||||
return match ($id) {
|
||||
101 => $mock1,
|
||||
default => false,
|
||||
};
|
||||
});
|
||||
|
||||
$product = $this->createProductType();
|
||||
$price = $product->calculate_composed_price([101, 999]);
|
||||
|
||||
$this->assertSame(5.0, $price);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
/**
|
||||
* StockManager Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\StockManager;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class StockManagerTest extends TestCase
|
||||
{
|
||||
private StockManager $manager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->manager = new StockManager();
|
||||
}
|
||||
|
||||
// --- validate_stock_availability ---
|
||||
|
||||
public function testValidateStock_ReturnsTrueWhenAllInStock(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => false,
|
||||
'is_in_stock' => true,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
Functions\expect('wc_get_product')->with(2)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1, 2]);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateStock_ReturnsTrueWhenNotManagingStock(): void
|
||||
{
|
||||
$mock = $this->createProductMock(['managing_stock' => false]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1]);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateStock_ReturnsErrorForOutOfStock(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => true,
|
||||
'is_in_stock' => false,
|
||||
'get_name' => 'Widget',
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1]);
|
||||
$this->assertIsString($result);
|
||||
$this->assertStringContainsString('Widget', $result);
|
||||
}
|
||||
|
||||
public function testValidateStock_ReturnsErrorForInsufficientQuantity(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => true,
|
||||
'is_in_stock' => true,
|
||||
'get_stock_quantity' => 1,
|
||||
'get_name' => 'Widget',
|
||||
'backorders_allowed' => false,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1], 5);
|
||||
$this->assertIsString($result);
|
||||
$this->assertStringContainsString('Widget', $result);
|
||||
}
|
||||
|
||||
public function testValidateStock_PassesWhenBackordersAllowed(): void
|
||||
{
|
||||
// When stock_quantity is null the insufficient-stock check is skipped,
|
||||
// and the backorders_allowed() branch is reached.
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => true,
|
||||
'is_in_stock' => true,
|
||||
'get_stock_quantity' => null,
|
||||
'backorders_allowed' => true,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1], 5);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateStock_SkipsNullProducts(): void
|
||||
{
|
||||
Functions\expect('wc_get_product')->with(999)->andReturn(false);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([999]);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
// --- get_product_stock_info ---
|
||||
|
||||
public function testGetProductStockInfo_ReturnsCorrectStructure(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'is_in_stock' => true,
|
||||
'get_stock_quantity' => 10,
|
||||
'backorders_allowed' => false,
|
||||
'get_stock_status' => 'instock',
|
||||
'managing_stock' => true,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$info = $this->manager->get_product_stock_info(1);
|
||||
|
||||
$this->assertTrue($info['in_stock']);
|
||||
$this->assertSame(10, $info['stock_quantity']);
|
||||
$this->assertFalse($info['backorders_allowed']);
|
||||
$this->assertSame('instock', $info['stock_status']);
|
||||
$this->assertTrue($info['managing_stock']);
|
||||
$this->assertTrue($info['has_enough_stock']);
|
||||
}
|
||||
|
||||
public function testGetProductStockInfo_ReturnsFallbackForInvalidProduct(): void
|
||||
{
|
||||
Functions\expect('wc_get_product')->with(999)->andReturn(false);
|
||||
|
||||
$info = $this->manager->get_product_stock_info(999);
|
||||
|
||||
$this->assertFalse($info['in_stock']);
|
||||
$this->assertSame(0, $info['stock_quantity']);
|
||||
$this->assertFalse($info['backorders_allowed']);
|
||||
$this->assertSame('outofstock', $info['stock_status']);
|
||||
}
|
||||
|
||||
public function testGetProductStockInfo_HasEnoughStockTrueWhenNotManaging(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => false,
|
||||
'get_stock_quantity' => null,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$info = $this->manager->get_product_stock_info(1, 100);
|
||||
$this->assertTrue($info['has_enough_stock']);
|
||||
}
|
||||
|
||||
public function testGetProductStockInfo_HasEnoughStockFalseWhenInsufficient(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => true,
|
||||
'get_stock_quantity' => 2,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$info = $this->manager->get_product_stock_info(1, 5);
|
||||
$this->assertFalse($info['has_enough_stock']);
|
||||
}
|
||||
|
||||
// --- prevent_composable_stock_reduction ---
|
||||
|
||||
public function testPreventStockReduction_ReturnsFalseForComposableItem(): void
|
||||
{
|
||||
$productMock = $this->createProductMock(['get_type' => 'composable']);
|
||||
|
||||
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||
$itemMock->shouldReceive('get_product')->andReturn($productMock);
|
||||
|
||||
$orderMock = \Mockery::mock('WC_Order');
|
||||
$orderMock->shouldReceive('get_items')->andReturn([$itemMock]);
|
||||
|
||||
$result = $this->manager->prevent_composable_stock_reduction(true, $orderMock);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testPreventStockReduction_PassesThroughForNonComposable(): void
|
||||
{
|
||||
$productMock = $this->createProductMock(['get_type' => 'simple']);
|
||||
|
||||
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||
$itemMock->shouldReceive('get_product')->andReturn($productMock);
|
||||
|
||||
$orderMock = \Mockery::mock('WC_Order');
|
||||
$orderMock->shouldReceive('get_items')->andReturn([$itemMock]);
|
||||
|
||||
$result = $this->manager->prevent_composable_stock_reduction(true, $orderMock);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
// --- store_selected_products_in_order ---
|
||||
|
||||
public function testStoreSelectedProducts_AddsMetaWhenPresent(): void
|
||||
{
|
||||
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||
$itemMock->shouldReceive('add_meta_data')
|
||||
->once()
|
||||
->with('_composable_products', [1, 2], true);
|
||||
|
||||
$this->manager->store_selected_products_in_order(
|
||||
$itemMock,
|
||||
'cart_key',
|
||||
['composable_products' => [1, 2]]
|
||||
);
|
||||
}
|
||||
|
||||
public function testStoreSelectedProducts_DoesNothingWithoutData(): void
|
||||
{
|
||||
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||
$itemMock->shouldNotReceive('add_meta_data');
|
||||
|
||||
$this->manager->store_selected_products_in_order(
|
||||
$itemMock,
|
||||
'cart_key',
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* PHPUnit Bootstrap
|
||||
*
|
||||
* Sets up WooCommerce stubs, plugin constants, and the Composer autoloader
|
||||
* for running unit tests without a full WordPress installation.
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
// Composer autoloader (loads Brain Monkey, Mockery, plugin classes)
|
||||
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
// Define WordPress constants that the plugin expects
|
||||
defined('ABSPATH') || define('ABSPATH', '/tmp/wordpress/');
|
||||
defined('WP_DEBUG') || define('WP_DEBUG', true);
|
||||
defined('DOING_AJAX') || define('DOING_AJAX', false);
|
||||
|
||||
// Define plugin constants
|
||||
defined('WC_COMPOSABLE_PRODUCT_VERSION') || define('WC_COMPOSABLE_PRODUCT_VERSION', '1.3.1');
|
||||
defined('WC_COMPOSABLE_PRODUCT_FILE') || define('WC_COMPOSABLE_PRODUCT_FILE', dirname(__DIR__) . '/wc-composable-product.php');
|
||||
defined('WC_COMPOSABLE_PRODUCT_PATH') || define('WC_COMPOSABLE_PRODUCT_PATH', dirname(__DIR__) . '/');
|
||||
defined('WC_COMPOSABLE_PRODUCT_URL') || define('WC_COMPOSABLE_PRODUCT_URL', 'https://example.com/wp-content/plugins/wc-composable-product/');
|
||||
defined('WC_COMPOSABLE_PRODUCT_BASENAME') || define('WC_COMPOSABLE_PRODUCT_BASENAME', 'wc-composable-product/wc-composable-product.php');
|
||||
|
||||
// Load WooCommerce class stubs (parent before child)
|
||||
require_once __DIR__ . '/stubs/class-wc-data.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-product.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-settings-page.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-order.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-order-item-product.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-cart.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-admin-settings.php';
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Admin_Settings stub for unit testing.
|
||||
*/
|
||||
class WC_Admin_Settings {
|
||||
public static function output_fields($options) {
|
||||
}
|
||||
|
||||
public static function save_fields($options) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Cart stub for unit testing.
|
||||
*/
|
||||
class WC_Cart {
|
||||
protected $cart_contents = [];
|
||||
|
||||
public function get_cart() {
|
||||
return $this->cart_contents;
|
||||
}
|
||||
|
||||
public function set_cart_contents($contents) {
|
||||
$this->cart_contents = $contents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Data stub for unit testing.
|
||||
*/
|
||||
class WC_Data {
|
||||
protected $id = 0;
|
||||
protected $data = [];
|
||||
protected $meta_data = [];
|
||||
|
||||
public function __construct($id = 0) {
|
||||
if (is_numeric($id) && $id > 0) {
|
||||
$this->id = (int) $id;
|
||||
}
|
||||
}
|
||||
|
||||
public function get_id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function set_id($id) {
|
||||
$this->id = (int) $id;
|
||||
}
|
||||
|
||||
public function get_meta($key, $single = true, $context = 'view') {
|
||||
return $this->meta_data[$key] ?? ($single ? '' : []);
|
||||
}
|
||||
|
||||
public function update_meta_data($key, $value, $meta_id = 0) {
|
||||
$this->meta_data[$key] = $value;
|
||||
}
|
||||
|
||||
public function save() {
|
||||
return $this->get_id();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Order_Item_Product stub for unit testing.
|
||||
*/
|
||||
class WC_Order_Item_Product extends WC_Data {
|
||||
protected $product = null;
|
||||
protected $quantity = 1;
|
||||
|
||||
public function get_product() {
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function set_product($product) {
|
||||
$this->product = $product;
|
||||
}
|
||||
|
||||
public function get_quantity() {
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function set_quantity($quantity) {
|
||||
$this->quantity = $quantity;
|
||||
}
|
||||
|
||||
public function add_meta_data($key, $value, $unique = false) {
|
||||
$this->meta_data[$key] = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Order stub for unit testing.
|
||||
*/
|
||||
class WC_Order extends WC_Data {
|
||||
protected $items = [];
|
||||
protected $order_notes = [];
|
||||
|
||||
public function get_items($type = '') {
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function set_items($items) {
|
||||
$this->items = $items;
|
||||
}
|
||||
|
||||
public function add_order_note($note) {
|
||||
$this->order_notes[] = $note;
|
||||
}
|
||||
|
||||
public function get_order_notes() {
|
||||
return $this->order_notes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Product stub for unit testing.
|
||||
*/
|
||||
class WC_Product extends WC_Data {
|
||||
protected $supports = [];
|
||||
protected $data = [
|
||||
'name' => '',
|
||||
'price' => '',
|
||||
'regular_price' => '',
|
||||
'status' => 'publish',
|
||||
];
|
||||
|
||||
public function get_type() {
|
||||
return 'simple';
|
||||
}
|
||||
|
||||
public function get_name($context = 'view') {
|
||||
return $this->data['name'] ?? '';
|
||||
}
|
||||
|
||||
public function get_price($context = 'view') {
|
||||
return $this->data['price'] ?? '';
|
||||
}
|
||||
|
||||
public function get_regular_price($context = 'view') {
|
||||
return $this->data['regular_price'] ?? '';
|
||||
}
|
||||
|
||||
public function get_price_html() {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function get_permalink() {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function get_image_id($context = 'view') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function get_stock_quantity($context = 'view') {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_stock_status($context = 'view') {
|
||||
return 'instock';
|
||||
}
|
||||
|
||||
public function get_children() {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function is_type($type) {
|
||||
return $this->get_type() === $type;
|
||||
}
|
||||
|
||||
public function is_purchasable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function is_in_stock() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function managing_stock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function backorders_allowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function is_sold_individually() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function set_price($price) {
|
||||
$this->data['price'] = $price;
|
||||
}
|
||||
|
||||
public function set_stock_quantity($quantity) {
|
||||
}
|
||||
|
||||
public function supports($feature) {
|
||||
return in_array($feature, $this->supports, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Settings_Page stub for unit testing.
|
||||
*/
|
||||
class WC_Settings_Page {
|
||||
protected $id = '';
|
||||
protected $label = '';
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public function get_id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function get_label() {
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function get_settings() {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function output() {
|
||||
}
|
||||
|
||||
public function save() {
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Plugin Name: WooCommerce Composable Products
|
||||
* Plugin URI: https://github.com/magdev/wc-composable-product
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-composable-product
|
||||
* Description: Create composable products where customers select a limited number of items from a configurable set
|
||||
* Version: 1.1.10
|
||||
* Version: 1.3.1
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://example.com
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL v3 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
* Text Domain: wc-composable-product
|
||||
@@ -19,7 +20,7 @@
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// Define plugin constants
|
||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.1.10');
|
||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.3.1');
|
||||
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
||||
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
||||
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
||||
@@ -59,7 +60,7 @@ function wc_composable_product_init() {
|
||||
load_plugin_textdomain('wc-composable-product', false, dirname(WC_COMPOSABLE_PRODUCT_BASENAME) . '/languages');
|
||||
|
||||
// Initialize main plugin class
|
||||
WC_Composable_Product\Plugin::instance();
|
||||
Magdev\WcComposableProduct\Plugin::instance();
|
||||
}
|
||||
// Use woocommerce_init to ensure all WooCommerce classes including settings are loaded
|
||||
add_action('woocommerce_init', 'wc_composable_product_init');
|
||||
|
||||
Reference in new issue
Block a user